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 ChainTooLong,
214 SignatureExpired(u32),
215 MailFromMismatch(u32),
216 RcptToMismatch(u32),
217 MailFromDomainMismatch(u32),
218 NextDomainMismatch(u32),
219 CustodyBreak(u32),
220 PublicKeyFetch(u32),
221 PublicKeyMissing(u32),
222 PublicKeyMultiple(u32),
223 PublicKeySyntax(u32),
224 PublicKeyAlgorithmMismatch(u32),
225 PublicKeyRevoked(u32),
226 IncorrectSignature(u32),
227 NoValidAlgorithm(u32),
228 HeaderHashMismatch(u32),
229 BodyHashMismatch(u32),
230 Modified,
231 Exploded,
232}
233
234impl std::fmt::Display for Dkim2Error {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 match self {
237 Dkim2Error::InstanceMissing(m) => write!(f, "Message-Instance m={m} missing"),
238 Dkim2Error::InstanceSyntax(m) => write!(f, "Message-Instance m={m} syntax error"),
239 Dkim2Error::InstanceTagMissing { m, tag } => {
240 write!(f, "Message-Instance m={m} tag={tag} missing")
241 }
242 Dkim2Error::InstanceNotSigned(m) => write!(f, "Message-Instance m={m} is not signed"),
243 Dkim2Error::InstanceAboveSignature(m) => {
244 write!(
245 f,
246 "Message-Instance m={m} is higher than any DKIM2-Signature"
247 )
248 }
249 Dkim2Error::SignatureMissing(i) => write!(f, "DKIM2-Signature i={i} missing"),
250 Dkim2Error::SignatureSyntax(i) => write!(f, "DKIM2-Signature i={i} syntax error"),
251 Dkim2Error::SignatureTagMissing { i, tag } => {
252 write!(f, "DKIM2-Signature i={i} tag={tag} missing")
253 }
254 Dkim2Error::SignatureTagUnexpected { i, tag } => {
255 write!(f, "DKIM2-Signature i={i} tag={tag} was unexpected")
256 }
257 Dkim2Error::SequenceGap => write!(f, "DKIM2 sequence numbering has a gap"),
258 Dkim2Error::SequenceOverflow => {
259 write!(f, "DKIM2 sequence numbering would overflow")
260 }
261 Dkim2Error::ChainTooLong => write!(f, "Too many DKIM2 header fields"),
262 Dkim2Error::SignatureExpired(i) => write!(f, "DKIM2-Signature i={i} signature expired"),
263 Dkim2Error::MailFromMismatch(i) => {
264 write!(f, "DKIM2-Signature i={i} MAIL FROM did not match")
265 }
266 Dkim2Error::RcptToMismatch(i) => {
267 write!(f, "DKIM2-Signature i={i} RCPT TO did not match")
268 }
269 Dkim2Error::MailFromDomainMismatch(i) => {
270 write!(f, "DKIM2-Signature i={i} MAIL FROM and d= do not match")
271 }
272 Dkim2Error::NextDomainMismatch(i) => {
273 write!(f, "DKIM2-Signature i={i} nd= does not match")
274 }
275 Dkim2Error::CustodyBreak(i) => {
276 write!(
277 f,
278 "DKIM2-Signature i={i} d= and previous RCPT TO do not match"
279 )
280 }
281 Dkim2Error::PublicKeyFetch(i) => {
282 write!(f, "DKIM2-Signature i={i} public key could not be fetched")
283 }
284 Dkim2Error::PublicKeyMissing(i) => {
285 write!(f, "DKIM2-Signature i={i} public key does not exist")
286 }
287 Dkim2Error::PublicKeyMultiple(i) => {
288 write!(f, "DKIM2-Signature i={i} public key has multiple records")
289 }
290 Dkim2Error::PublicKeySyntax(i) => {
291 write!(f, "DKIM2-Signature i={i} public key has a syntax error")
292 }
293 Dkim2Error::PublicKeyAlgorithmMismatch(i) => {
294 write!(f, "DKIM2-Signature i={i} public key algorithm mismatch")
295 }
296 Dkim2Error::PublicKeyRevoked(i) => {
297 write!(f, "DKIM2-Signature i={i} public key has been revoked")
298 }
299 Dkim2Error::IncorrectSignature(i) => {
300 write!(f, "DKIM2-Signature i={i} incorrect signature")
301 }
302 Dkim2Error::NoValidAlgorithm(i) => {
303 write!(f, "DKIM2-Signature i={i} has no valid signature algorithms")
304 }
305 Dkim2Error::HeaderHashMismatch(m) => {
306 write!(f, "Message-Instance m={m} header hash mismatch")
307 }
308 Dkim2Error::BodyHashMismatch(m) => {
309 write!(f, "Message-Instance m={m} body hash mismatch")
310 }
311 Dkim2Error::Modified => {
312 write!(f, "Message has been modified despite a donotmodify request")
313 }
314 Dkim2Error::Exploded => {
315 write!(
316 f,
317 "Message has been exploded despite a donotexplode request"
318 )
319 }
320 }
321 }
322}
323
324#[cfg(test)]
325mod test {
326 use super::*;
327
328 fn signed(i: u32, domain: &str, flags: Vec<Flag>) -> Signature {
329 Signature {
330 i,
331 d: domain.to_string(),
332 flags,
333 ..Default::default()
334 }
335 }
336
337 #[test]
338 fn feedback_accessors() {
339 let sig1 = signed(1, "a.example", vec![Flag::Feedback]);
340 let sig2 = signed(2, "b.example", vec![Flag::Feedback, Flag::FeedHere]);
341 let link = |signature| ChainLink {
342 signature,
343 instance: None,
344 result: Dkim2Result::Pass,
345 custody_ok: true,
346 };
347 let output = Dkim2Output {
348 result: Dkim2Result::Pass,
349 chain: vec![link(&sig1), link(&sig2)],
350 };
351
352 assert!(output.feedback_requested());
353 assert_eq!(output.feedback_domains(), vec!["a.example", "b.example"]);
354 assert_eq!(output.feedback_relay(), Some("b.example"));
355
356 let none = Dkim2Output::from(Dkim2Result::Pass);
357 assert!(!none.feedback_requested());
358 assert!(none.feedback_domains().is_empty());
359 assert_eq!(none.feedback_relay(), None);
360 }
361}