1use super::headers::{HeaderWriter, Writer};
8use crate::{
9 ArcOutput, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput, DmarcResult, Error,
10 IprevOutput, IprevResult, ReceivedSpf, SpfOutput, SpfResult,
11};
12use mail_builder::encoders::base64::base64_encode;
13use std::{
14 borrow::Cow,
15 fmt::{Display, Write},
16 net::IpAddr,
17};
18
19impl<'x> AuthenticationResults<'x> {
20 pub fn new(hostname: &'x str) -> Self {
21 AuthenticationResults {
22 hostname,
23 auth_results: String::with_capacity(64),
24 }
25 }
26
27 pub fn with_dkim_results(mut self, dkim: &[DkimOutput], header_from: &str) -> Self {
28 for dkim in dkim {
29 self.set_dkim_result(dkim, header_from);
30 }
31 self
32 }
33
34 pub fn with_dkim_result(mut self, dkim: &DkimOutput, header_from: &str) -> Self {
35 self.set_dkim_result(dkim, header_from);
36 self
37 }
38
39 pub fn set_dkim_result(&mut self, dkim: &DkimOutput, header_from: &str) {
40 if !dkim.is_atps {
41 self.auth_results.push_str(";\r\n\tdkim=");
42 } else {
43 self.auth_results.push_str(";\r\n\tdkim-atps=");
44 }
45 dkim.result.as_auth_result(&mut self.auth_results);
46 if let Some(signature) = &dkim.signature {
47 if !signature.i.is_empty() {
48 self.auth_results.push_str(" header.i=");
49 self.auth_results.push_str(&signature.i);
50 } else {
51 self.auth_results.push_str(" header.d=");
52 self.auth_results.push_str(&signature.d);
53 }
54 self.auth_results.push_str(" header.s=");
55 self.auth_results.push_str(&signature.s);
56 if signature.b.len() >= 6 {
57 self.auth_results.push_str(" header.b=");
58 self.auth_results.push_str(
59 &String::from_utf8(base64_encode(&signature.b[..6]).unwrap_or_default())
60 .unwrap_or_default(),
61 );
62 }
63 }
64
65 if dkim.is_atps {
66 write!(self.auth_results, " header.from={header_from}").ok();
67 }
68 }
69
70 pub fn with_spf_ehlo_result(
71 mut self,
72 spf: &SpfOutput,
73 ip_addr: IpAddr,
74 ehlo_domain: &str,
75 ) -> Self {
76 self.auth_results.push_str(";\r\n\tspf=");
77 spf.result.as_spf_result(
78 &mut self.auth_results,
79 self.hostname,
80 &format!("postmaster@{ehlo_domain}"),
81 ip_addr,
82 );
83 write!(self.auth_results, " smtp.helo={ehlo_domain}").ok();
84 self
85 }
86
87 pub fn with_spf_mailfrom_result(
88 mut self,
89 spf: &SpfOutput,
90 ip_addr: IpAddr,
91 from: &str,
92 ehlo_domain: &str,
93 ) -> Self {
94 let (mail_from, addr) = if !from.is_empty() {
95 (Cow::from(from), from)
96 } else {
97 (format!("postmaster@{ehlo_domain}").into(), "<>")
98 };
99 self.auth_results.push_str(";\r\n\tspf=");
100 spf.result.as_spf_result(
101 &mut self.auth_results,
102 self.hostname,
103 mail_from.as_ref(),
104 ip_addr,
105 );
106 write!(self.auth_results, " smtp.mailfrom={addr}").ok();
107 self
108 }
109
110 pub fn with_arc_result(mut self, arc: &ArcOutput, remote_ip: IpAddr) -> Self {
111 self.auth_results.push_str(";\r\n\tarc=");
112 arc.result.as_auth_result(&mut self.auth_results);
113 let _ = write!(self.auth_results, " smtp.remote-ip=");
114 let _ = format_ip_as_pvalue(&mut self.auth_results, remote_ip);
115 self
116 }
117
118 pub fn with_dmarc_result(mut self, dmarc: &DmarcOutput) -> Self {
119 self.auth_results.push_str(";\r\n\tdmarc=");
120 if dmarc.spf_result == DmarcResult::Pass || dmarc.dkim_result == DmarcResult::Pass {
121 DmarcResult::Pass.as_auth_result(&mut self.auth_results);
122 } else if dmarc.spf_result != DmarcResult::None {
123 dmarc.spf_result.as_auth_result(&mut self.auth_results);
124 } else if dmarc.dkim_result != DmarcResult::None {
125 dmarc.dkim_result.as_auth_result(&mut self.auth_results);
126 } else {
127 DmarcResult::None.as_auth_result(&mut self.auth_results);
128 }
129 write!(
130 self.auth_results,
131 " header.from={} policy.dmarc={}",
132 dmarc.domain, dmarc.policy
133 )
134 .ok();
135 self
136 }
137
138 pub fn with_iprev_result(mut self, iprev: &IprevOutput, remote_ip: IpAddr) -> Self {
139 self.auth_results.push_str(";\r\n\tiprev=");
140 iprev.result.as_auth_result(&mut self.auth_results);
141 let _ = write!(self.auth_results, " policy.iprev=");
142 let _ = format_ip_as_pvalue(&mut self.auth_results, remote_ip);
143 self
144 }
145}
146
147impl Display for AuthenticationResults<'_> {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.write_str(self.hostname)?;
150 f.write_str(&self.auth_results)
151 }
152}
153
154impl HeaderWriter for AuthenticationResults<'_> {
155 fn write_header(&self, writer: &mut impl Writer) {
156 writer.write(b"Authentication-Results: ");
157 writer.write(self.hostname.as_bytes());
158 if !self.auth_results.is_empty() {
159 writer.write(self.auth_results.as_bytes());
160 } else {
161 writer.write(b"; none");
162 }
163 writer.write(b"\r\n");
164 }
165}
166
167impl HeaderWriter for ReceivedSpf {
168 fn write_header(&self, writer: &mut impl Writer) {
169 writer.write(b"Received-SPF: ");
170 writer.write(self.received_spf.as_bytes());
171 writer.write(b"\r\n");
172 }
173}
174
175impl ReceivedSpf {
176 pub fn new(
177 spf: &SpfOutput,
178 ip_addr: IpAddr,
179 helo: &str,
180 mail_from: &str,
181 hostname: &str,
182 ) -> Self {
183 let mut received_spf = String::with_capacity(64);
184 let mail_from = if !mail_from.is_empty() {
185 Cow::from(mail_from)
186 } else {
187 format!("postmaster@{helo}").into()
188 };
189
190 spf.result
191 .as_spf_result(&mut received_spf, hostname, mail_from.as_ref(), ip_addr);
192
193 write!(
194 received_spf,
195 "\r\n\treceiver={hostname}; client-ip={ip_addr}; envelope-from=\"{mail_from}\"; helo={helo};",
196 )
197 .ok();
198
199 ReceivedSpf { received_spf }
200 }
201}
202
203impl SpfResult {
204 fn as_spf_result(&self, header: &mut String, hostname: &str, mail_from: &str, ip_addr: IpAddr) {
205 match &self {
206 SpfResult::Pass => write!(
207 header,
208 "pass ({hostname}: domain of {mail_from} designates {ip_addr} as permitted sender)",
209 ),
210 SpfResult::Fail => write!(
211 header,
212 "fail ({hostname}: domain of {mail_from} does not designate {ip_addr} as permitted sender)",
213 ),
214 SpfResult::SoftFail => write!(
215 header,
216 "softfail ({hostname}: domain of {mail_from} reports soft fail for {ip_addr})",
217 ),
218 SpfResult::Neutral => write!(
219 header,
220 "neutral ({hostname}: domain of {mail_from} reports neutral for {ip_addr})",
221 ),
222 SpfResult::TempError => write!(
223 header,
224 "temperror ({hostname}: temporary dns error validating {mail_from})",
225 ),
226 SpfResult::PermError => write!(
227 header,
228 "permerror ({hostname}: unable to verify SPF record for {mail_from})",
229 ),
230 SpfResult::None => write!(
231 header,
232 "none ({hostname}: no SPF records found for {mail_from})",
233 ),
234 }
235 .ok();
236 }
237}
238
239pub trait AsAuthResult {
240 fn as_auth_result(&self, header: &mut String);
241}
242
243impl AsAuthResult for DmarcResult {
244 fn as_auth_result(&self, header: &mut String) {
245 match &self {
246 DmarcResult::Pass => header.push_str("pass"),
247 DmarcResult::Fail(err) => {
248 header.push_str("fail");
249 err.as_auth_result(header);
250 }
251 DmarcResult::PermError(err) => {
252 header.push_str("permerror");
253 err.as_auth_result(header);
254 }
255 DmarcResult::TempError(err) => {
256 header.push_str("temperror");
257 err.as_auth_result(header);
258 }
259 DmarcResult::None => header.push_str("none"),
260 }
261 }
262}
263
264impl AsAuthResult for IprevResult {
265 fn as_auth_result(&self, header: &mut String) {
266 match &self {
267 IprevResult::Pass => header.push_str("pass"),
268 IprevResult::Fail(err) => {
269 header.push_str("fail");
270 err.as_auth_result(header);
271 }
272 IprevResult::PermError(err) => {
273 header.push_str("permerror");
274 err.as_auth_result(header);
275 }
276 IprevResult::TempError(err) => {
277 header.push_str("temperror");
278 err.as_auth_result(header);
279 }
280 IprevResult::None => header.push_str("none"),
281 }
282 }
283}
284
285impl AsAuthResult for DkimResult {
286 fn as_auth_result(&self, header: &mut String) {
287 match &self {
288 DkimResult::Pass => header.push_str("pass"),
289 DkimResult::Neutral(err) => {
290 header.push_str("neutral");
291 err.as_auth_result(header);
292 }
293 DkimResult::Fail(err) => {
294 header.push_str("fail");
295 err.as_auth_result(header);
296 }
297 DkimResult::PermError(err) => {
298 header.push_str("permerror");
299 err.as_auth_result(header);
300 }
301 DkimResult::TempError(err) => {
302 header.push_str("temperror");
303 err.as_auth_result(header);
304 }
305 DkimResult::None => header.push_str("none"),
306 }
307 }
308}
309
310impl AsAuthResult for Error {
311 fn as_auth_result(&self, header: &mut String) {
312 header.push_str(" (");
313 header.push_str(match self {
314 Error::ParseError => "dns record parse error",
315 Error::MissingParameters => "missing parameters",
316 Error::NoHeadersFound => "no headers found",
317 Error::CryptoError(_) => "verification failed",
318 Error::Io(_) => "i/o error",
319 Error::Base64 => "base64 error",
320 Error::UnsupportedVersion => "unsupported version",
321 Error::UnsupportedAlgorithm => "unsupported algorithm",
322 Error::UnsupportedCanonicalization => "unsupported canonicalization",
323 Error::UnsupportedKeyType => "unsupported key type",
324 Error::FailedBodyHashMatch => "body hash did not verify",
325 Error::FailedVerification => "verification failed",
326 Error::FailedAuidMatch => "auid does not match",
327 Error::RevokedPublicKey => "revoked public key",
328 Error::IncompatibleAlgorithms => "incompatible record/signature algorithms",
329 Error::SignatureExpired => "signature error",
330 Error::DnsError(_) => "dns error",
331 Error::DnsRecordNotFound(_) => "dns record not found",
332 Error::ArcInvalidInstance(i) => {
333 write!(header, "invalid ARC instance {i})").ok();
334 return;
335 }
336 Error::ArcInvalidCV => "invalid ARC cv",
337 Error::ArcChainTooLong => "too many ARC headers",
338 Error::ArcHasHeaderTag => "ARC has header tag",
339 Error::ArcBrokenChain => "broken ARC chain",
340 Error::NotAligned => "policy not aligned",
341 Error::InvalidRecordType => "invalid dns record type",
342 Error::SignatureLength => "signature length ignored due to security risk",
343 });
344 header.push(')');
345 }
346}
347
348fn format_ip_as_pvalue(w: &mut impl Write, ip: IpAddr) -> std::fmt::Result {
355 match ip {
356 IpAddr::V4(addr) => write!(w, "{addr}"),
357 IpAddr::V6(addr) => write!(w, "\"{addr}\""),
358 }
359}
360
361#[cfg(test)]
362mod test {
363 use crate::{
364 ArcOutput, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput, DmarcResult, Error,
365 IprevOutput, IprevResult, ReceivedSpf, SpfOutput, SpfResult, dkim::Signature,
366 dmarc::Policy,
367 };
368
369 #[test]
370 fn authentication_results() {
371 let mut auth_results = AuthenticationResults::new("mydomain.org");
372
373 for (expected_auth_results, dkim) in [
374 (
375 "dkim=pass header.d=example.org header.s=myselector",
376 DkimOutput {
377 result: DkimResult::Pass,
378 signature: (&Signature {
379 d: "example.org".into(),
380 s: "myselector".into(),
381 ..Default::default()
382 })
383 .into(),
384 report: None,
385 is_atps: false,
386 },
387 ),
388 (
389 concat!(
390 "dkim=fail (verification failed) header.d=example.org ",
391 "header.s=myselector header.b=MTIzNDU2"
392 ),
393 DkimOutput {
394 result: DkimResult::Fail(Error::FailedVerification),
395 signature: (&Signature {
396 d: "example.org".into(),
397 s: "myselector".into(),
398 b: b"123456".to_vec(),
399 ..Default::default()
400 })
401 .into(),
402 report: None,
403 is_atps: false,
404 },
405 ),
406 (
407 concat!(
408 "dkim-atps=temperror (dns error) header.d=atps.example.org ",
409 "header.s=otherselctor header.b=YWJjZGVm header.from=jdoe@example.org"
410 ),
411 DkimOutput {
412 result: DkimResult::TempError(Error::DnsError("".to_string())),
413 signature: (&Signature {
414 d: "atps.example.org".into(),
415 s: "otherselctor".into(),
416 b: b"abcdef".to_vec(),
417 ..Default::default()
418 })
419 .into(),
420 report: None,
421 is_atps: true,
422 },
423 ),
424 ] {
425 auth_results = auth_results.with_dkim_results(&[dkim], "jdoe@example.org");
426 assert_eq!(
427 auth_results.auth_results.rsplit_once(';').unwrap().1.trim(),
428 expected_auth_results
429 );
430 }
431
432 for (
433 expected_auth_results,
434 expected_received_spf,
435 result,
436 ip_addr,
437 receiver,
438 helo,
439 mail_from,
440 ) in [
441 (
442 concat!(
443 "spf=pass (localhost: domain of jdoe@example.org designates 192.168.1.1 ",
444 "as permitted sender) smtp.mailfrom=jdoe@example.org"
445 ),
446 concat!(
447 "pass (localhost: domain of jdoe@example.org designates 192.168.1.1 as ",
448 "permitted sender)\r\n\treceiver=localhost; client-ip=192.168.1.1; ",
449 "envelope-from=\"jdoe@example.org\"; helo=example.org;"
450 ),
451 SpfResult::Pass,
452 "192.168.1.1".parse().unwrap(),
453 "localhost",
454 "example.org",
455 "jdoe@example.org",
456 ),
457 (
458 concat!(
459 "spf=fail (mx.domain.org: domain of sender@otherdomain.org does not ",
460 "designate a:b:c::f as permitted sender) smtp.mailfrom=sender@otherdomain.org"
461 ),
462 concat!(
463 "fail (mx.domain.org: domain of sender@otherdomain.org does not designate ",
464 "a:b:c::f as permitted sender)\r\n\treceiver=mx.domain.org; ",
465 "client-ip=a:b:c::f; envelope-from=\"sender@otherdomain.org\"; ",
466 "helo=otherdomain.org;"
467 ),
468 SpfResult::Fail,
469 "a:b:c::f".parse().unwrap(),
470 "mx.domain.org",
471 "otherdomain.org",
472 "sender@otherdomain.org",
473 ),
474 (
475 concat!(
476 "spf=neutral (mx.domain.org: domain of postmaster@example.org reports neutral ",
477 "for a:b:c::f) smtp.mailfrom=<>"
478 ),
479 concat!(
480 "neutral (mx.domain.org: domain of postmaster@example.org reports neutral for ",
481 "a:b:c::f)\r\n\treceiver=mx.domain.org; client-ip=a:b:c::f; ",
482 "envelope-from=\"postmaster@example.org\"; helo=example.org;"
483 ),
484 SpfResult::Neutral,
485 "a:b:c::f".parse().unwrap(),
486 "mx.domain.org",
487 "example.org",
488 "",
489 ),
490 ] {
491 auth_results.hostname = receiver;
492 auth_results = auth_results.with_spf_mailfrom_result(
493 &SpfOutput {
494 result,
495 domain: "".to_string(),
496 report: None,
497 explanation: None,
498 },
499 ip_addr,
500 mail_from,
501 helo,
502 );
503 let received_spf = ReceivedSpf::new(
504 &SpfOutput {
505 result,
506 domain: "".to_string(),
507 report: None,
508 explanation: None,
509 },
510 ip_addr,
511 helo,
512 mail_from,
513 receiver,
514 );
515 assert_eq!(
516 auth_results.auth_results.rsplit_once(';').unwrap().1.trim(),
517 expected_auth_results
518 );
519 assert_eq!(received_spf.received_spf, expected_received_spf);
520 }
521
522 for (expected_auth_results, dmarc) in [
523 (
524 "dmarc=pass header.from=example.org policy.dmarc=none",
525 DmarcOutput {
526 spf_result: DmarcResult::Pass,
527 dkim_result: DmarcResult::None,
528 domain: "example.org".to_string(),
529 policy: Policy::None,
530 record: None,
531 },
532 ),
533 (
534 "dmarc=fail (policy not aligned) header.from=example.com policy.dmarc=quarantine",
535 DmarcOutput {
536 dkim_result: DmarcResult::Fail(Error::NotAligned),
537 spf_result: DmarcResult::None,
538 domain: "example.com".to_string(),
539 policy: Policy::Quarantine,
540 record: None,
541 },
542 ),
543 ] {
544 auth_results = auth_results.with_dmarc_result(&dmarc);
545 assert_eq!(
546 auth_results.auth_results.rsplit_once(';').unwrap().1.trim(),
547 expected_auth_results
548 );
549 }
550
551 for (expected_auth_results, arc, remote_ip) in [
552 (
553 "arc=pass smtp.remote-ip=192.127.9.2",
554 DkimResult::Pass,
555 "192.127.9.2".parse().unwrap(),
556 ),
557 (
558 "arc=neutral (body hash did not verify) smtp.remote-ip=\"1:2:3::a\"",
559 DkimResult::Neutral(Error::FailedBodyHashMatch),
560 "1:2:3::a".parse().unwrap(),
561 ),
562 ] {
563 auth_results = auth_results.with_arc_result(
564 &ArcOutput {
565 result: arc,
566 set: vec![],
567 },
568 remote_ip,
569 );
570 assert_eq!(
571 auth_results.auth_results.rsplit_once(';').unwrap().1.trim(),
572 expected_auth_results
573 );
574 }
575
576 for (expected_auth_results, iprev, remote_ip) in [
577 (
578 "iprev=pass policy.iprev=192.127.9.2",
579 IprevOutput {
580 result: IprevResult::Pass,
581 ptr: None,
582 },
583 "192.127.9.2".parse().unwrap(),
584 ),
585 (
586 "iprev=fail (policy not aligned) policy.iprev=\"1:2:3::a\"",
587 IprevOutput {
588 result: IprevResult::Fail(Error::NotAligned),
589 ptr: None,
590 },
591 "1:2:3::a".parse().unwrap(),
592 ),
593 ] {
594 auth_results = auth_results.with_iprev_result(&iprev, remote_ip);
595 assert_eq!(
596 auth_results.auth_results.rsplit_once(';').unwrap().1.trim(),
597 expected_auth_results
598 );
599 }
600 }
601}