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