1use crate::{
8 Dkim2Result,
9 common::crypto::{Algorithm, DkimKey, HashAlgorithm},
10};
11
12pub mod builder;
13pub mod canonicalize;
14pub mod dsn;
15pub mod headers;
16pub mod parse;
17pub mod recipe;
18mod recipe_serializer;
19pub mod sign;
20pub mod verify;
21
22#[cfg(test)]
23mod interop_test;
24
25pub use dsn::{Dkim2Dsn, Dkim2DsnFailure, Dkim2DsnOutput};
26pub use recipe::{BodyRecipe, HeaderRecipe, Recipe, Step};
27pub use sign::{Dkim2Signed, Envelope, Hop};
28
29pub struct NeedDomain;
30pub struct NeedSelector;
31pub struct Done;
32
33pub struct Dkim2Signer<State = NeedDomain> {
34 _state: std::marker::PhantomData<State>,
35 pub keys: Vec<KeyEntry>,
36 pub domain: String,
37 pub flags: Vec<Flag>,
38 pub nonce: Option<String>,
39}
40
41pub struct KeyEntry {
42 pub key: DkimKey,
43 pub selector: String,
44}
45
46#[derive(Debug, PartialEq, Eq, Clone, Default)]
47pub struct Signature {
48 pub i: u32,
49 pub m: u32,
50 pub t: u64,
51 pub d: String,
52 pub s: Vec<SignatureValue>,
53 pub chain: ChainBinding,
54 pub n: Option<String>,
55 pub flags: Vec<Flag>,
56}
57
58#[derive(Debug, PartialEq, Eq, Clone)]
59pub struct SignatureValue {
60 pub selector: String,
61 pub a: Algorithm,
62 pub b: Vec<u8>,
63}
64
65#[derive(Debug, PartialEq, Eq, Clone)]
66pub enum ChainBinding {
67 Envelope {
68 mail_from: String,
69 rcpt_to: Vec<String>,
70 },
71 NextDomain(String),
72}
73
74impl Default for ChainBinding {
75 fn default() -> Self {
76 ChainBinding::Envelope {
77 mail_from: String::new(),
78 rcpt_to: Vec::new(),
79 }
80 }
81}
82
83#[derive(Debug, PartialEq, Eq, Clone)]
84pub enum Flag {
85 DoNotModify,
86 DoNotExplode,
87 Feedback,
88 FeedHere,
89 Exploded,
90 Unknown(String),
91}
92
93impl Flag {
94 pub fn as_bytes(&self) -> &[u8] {
95 match self {
96 Flag::DoNotModify => b"donotmodify",
97 Flag::DoNotExplode => b"donotexplode",
98 Flag::Feedback => b"feedback",
99 Flag::FeedHere => b"feedhere",
100 Flag::Exploded => b"exploded",
101 Flag::Unknown(value) => value.as_bytes(),
102 }
103 }
104}
105
106#[derive(Debug, PartialEq, Eq, Clone, Default)]
107pub struct MessageInstance {
108 pub m: u32,
109 pub hashes: Vec<MessageHash>,
110 pub recipe: Option<Recipe>,
111}
112
113#[derive(Debug, PartialEq, Eq, Clone)]
114pub struct MessageHash {
115 pub name: Option<HashAlgorithm>,
116 pub header_hash: Vec<u8>,
117 pub body_hash: Vec<u8>,
118}
119
120#[derive(Debug, PartialEq, Eq, Clone)]
121pub struct Dkim2Output<'x> {
122 pub(crate) result: Dkim2Result,
123 pub(crate) chain: Vec<ChainLink<'x>>,
124}
125
126#[derive(Debug, PartialEq, Eq, Clone)]
127pub struct ChainLink<'x> {
128 pub signature: &'x Signature,
129 pub instance: Option<&'x MessageInstance>,
130 pub result: Dkim2Result,
131 pub custody_ok: bool,
132}
133
134impl<'x> Dkim2Output<'x> {
135 pub fn result(&self) -> &Dkim2Result {
136 &self.result
137 }
138
139 pub fn chain(&self) -> &[ChainLink<'x>] {
140 &self.chain
141 }
142
143 pub fn error(&self) -> Option<&crate::Error> {
144 match &self.result {
145 Dkim2Result::Fail(err) | Dkim2Result::PermError(err) | Dkim2Result::TempError(err) => {
146 Some(err)
147 }
148 Dkim2Result::Pass | Dkim2Result::None => None,
149 }
150 }
151
152 pub fn failure_reason(&self) -> Option<String> {
153 self.error().map(|err| err.to_string())
154 }
155
156 pub fn feedback_requested(&self) -> bool {
159 self.chain
160 .iter()
161 .any(|link| link.signature.flags.contains(&Flag::Feedback))
162 }
163
164 pub fn feedback_domains(&self) -> Vec<&str> {
166 self.chain
167 .iter()
168 .filter(|link| link.signature.flags.contains(&Flag::Feedback))
169 .map(|link| link.signature.d.as_str())
170 .collect()
171 }
172
173 pub fn feedback_relay(&self) -> Option<&str> {
177 self.chain
178 .iter()
179 .filter(|link| link.signature.flags.contains(&Flag::FeedHere))
180 .max_by_key(|link| link.signature.i)
181 .map(|link| link.signature.d.as_str())
182 }
183}
184
185impl From<Dkim2Result> for Dkim2Output<'_> {
186 fn from(result: Dkim2Result) -> Self {
187 Dkim2Output {
188 result,
189 chain: Vec::new(),
190 }
191 }
192}
193
194impl Signature {
195 pub fn domain(&self) -> &str {
196 &self.d
197 }
198}
199
200#[derive(Debug, PartialEq, Eq, Clone)]
201pub enum Dkim2Error {
202 InstanceMissing(u32),
203 InstanceSyntax(u32),
204 InstanceTagMissing { m: u32, tag: &'static str },
205 InstanceNotSigned(u32),
206 InstanceAboveSignature(u32),
207 SignatureMissing(u32),
208 SignatureSyntax(u32),
209 SignatureTagMissing { i: u32, tag: &'static str },
210 SignatureTagUnexpected { i: u32, tag: &'static str },
211 SequenceGap,
212 SequenceOverflow,
213 SignatureExpired(u32),
214 MailFromMismatch(u32),
215 RcptToMismatch(u32),
216 MailFromDomainMismatch(u32),
217 NextDomainMismatch(u32),
218 PublicKeyFetch(u32),
219 PublicKeyMissing(u32),
220 PublicKeyMultiple(u32),
221 PublicKeySyntax(u32),
222 PublicKeyAlgorithmMismatch(u32),
223 PublicKeyRevoked(u32),
224 IncorrectSignature(u32),
225 NoValidAlgorithm(u32),
226 HeaderHashMismatch(u32),
227 BodyHashMismatch(u32),
228 Modified,
229 Exploded,
230}
231
232impl std::fmt::Display for Dkim2Error {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 match self {
235 Dkim2Error::InstanceMissing(m) => write!(f, "Message-Instance m={m} missing"),
236 Dkim2Error::InstanceSyntax(m) => write!(f, "Message-Instance m={m} syntax error"),
237 Dkim2Error::InstanceTagMissing { m, tag } => {
238 write!(f, "Message-Instance m={m} tag={tag} missing")
239 }
240 Dkim2Error::InstanceNotSigned(m) => write!(f, "Message-Instance m={m} is not signed"),
241 Dkim2Error::InstanceAboveSignature(m) => {
242 write!(
243 f,
244 "Message-Instance m={m} is higher than any DKIM2-Signature"
245 )
246 }
247 Dkim2Error::SignatureMissing(i) => write!(f, "DKIM2-Signature i={i} missing"),
248 Dkim2Error::SignatureSyntax(i) => write!(f, "DKIM2-Signature i={i} syntax error"),
249 Dkim2Error::SignatureTagMissing { i, tag } => {
250 write!(f, "DKIM2-Signature i={i} tag={tag} missing")
251 }
252 Dkim2Error::SignatureTagUnexpected { i, tag } => {
253 write!(f, "DKIM2-Signature i={i} tag={tag} was unexpected")
254 }
255 Dkim2Error::SequenceGap => write!(f, "DKIM2 sequence numbering has a gap"),
256 Dkim2Error::SequenceOverflow => {
257 write!(f, "DKIM2 sequence numbering would overflow")
258 }
259 Dkim2Error::SignatureExpired(i) => write!(f, "DKIM2-Signature i={i} signature expired"),
260 Dkim2Error::MailFromMismatch(i) => {
261 write!(f, "DKIM2-Signature i={i} MAIL FROM did not match")
262 }
263 Dkim2Error::RcptToMismatch(i) => {
264 write!(f, "DKIM2-Signature i={i} RCPT TO did not match")
265 }
266 Dkim2Error::MailFromDomainMismatch(i) => {
267 write!(f, "DKIM2-Signature i={i} MAIL FROM and d= do not match")
268 }
269 Dkim2Error::NextDomainMismatch(i) => {
270 write!(f, "DKIM2-Signature i={i} nd= does not match")
271 }
272 Dkim2Error::PublicKeyFetch(i) => {
273 write!(f, "DKIM2-Signature i={i} public key could not be fetched")
274 }
275 Dkim2Error::PublicKeyMissing(i) => {
276 write!(f, "DKIM2-Signature i={i} public key does not exist")
277 }
278 Dkim2Error::PublicKeyMultiple(i) => {
279 write!(f, "DKIM2-Signature i={i} public key has multiple records")
280 }
281 Dkim2Error::PublicKeySyntax(i) => {
282 write!(f, "DKIM2-Signature i={i} public key has a syntax error")
283 }
284 Dkim2Error::PublicKeyAlgorithmMismatch(i) => {
285 write!(f, "DKIM2-Signature i={i} public key algorithm mismatch")
286 }
287 Dkim2Error::PublicKeyRevoked(i) => {
288 write!(f, "DKIM2-Signature i={i} public key has been revoked")
289 }
290 Dkim2Error::IncorrectSignature(i) => {
291 write!(f, "DKIM2-Signature i={i} incorrect signature")
292 }
293 Dkim2Error::NoValidAlgorithm(i) => {
294 write!(f, "DKIM2-Signature i={i} has no valid signature algorithms")
295 }
296 Dkim2Error::HeaderHashMismatch(m) => {
297 write!(f, "Message-Instance m={m} header hash mismatch")
298 }
299 Dkim2Error::BodyHashMismatch(m) => {
300 write!(f, "Message-Instance m={m} body hash mismatch")
301 }
302 Dkim2Error::Modified => {
303 write!(f, "Message has been modified despite a donotmodify request")
304 }
305 Dkim2Error::Exploded => {
306 write!(
307 f,
308 "Message has been exploded despite a donotexplode request"
309 )
310 }
311 }
312 }
313}
314
315#[cfg(test)]
316mod test {
317 use super::*;
318
319 fn signed(i: u32, domain: &str, flags: Vec<Flag>) -> Signature {
320 Signature {
321 i,
322 d: domain.to_string(),
323 flags,
324 ..Default::default()
325 }
326 }
327
328 #[test]
329 fn feedback_accessors() {
330 let sig1 = signed(1, "a.example", vec![Flag::Feedback]);
331 let sig2 = signed(2, "b.example", vec![Flag::Feedback, Flag::FeedHere]);
332 let link = |signature| ChainLink {
333 signature,
334 instance: None,
335 result: Dkim2Result::Pass,
336 custody_ok: true,
337 };
338 let output = Dkim2Output {
339 result: Dkim2Result::Pass,
340 chain: vec![link(&sig1), link(&sig2)],
341 };
342
343 assert!(output.feedback_requested());
344 assert_eq!(output.feedback_domains(), vec!["a.example", "b.example"]);
345 assert_eq!(output.feedback_relay(), Some("b.example"));
346
347 let none = Dkim2Output::from(Dkim2Result::Pass);
348 assert!(!none.feedback_requested());
349 assert!(none.feedback_domains().is_empty());
350 assert_eq!(none.feedback_relay(), None);
351 }
352}