1#[cfg(feature = "arc")]
22use crate::{ArcOutput, arc::ArcError};
23use crate::{
24 Dkim2Result, DkimOutput, DkimResult, DmarcOutput, DmarcResult, DnsError, Error, IprevOutput,
25 IprevResult, SpfOutput, SpfResult,
26 crypto::CryptoError,
27 dkim::DkimError,
28 dkim2::{Dkim2Output, Verification},
29 dmarc::Policy,
30 headers::{HeaderWriter, IntegerBuffer, Writer},
31 spf::{SpfIdentity, verify::SpfParameters},
32};
33use encodify::base64;
34use std::{
35 borrow::Cow,
36 fmt::{Display, Write},
37 net::{IpAddr, Ipv4Addr},
38};
39
40#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct AuthenticationResults<'x> {
82 pub(crate) hostname: &'x str,
83 pub(crate) auth_results: String,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ReceivedSpf {
93 pub(crate) received_spf: String,
94}
95
96impl<'x> AuthenticationResults<'x> {
97 pub fn new(hostname: &'x str) -> Self {
100 AuthenticationResults {
101 hostname,
102 auth_results: String::with_capacity(256),
103 }
104 }
105
106 pub fn with_dkim_results(mut self, dkim: &[DkimOutput], header_from: &str) -> Self {
109 self.set_dkim_results(dkim, header_from);
110 self
111 }
112
113 pub fn set_dkim_results(&mut self, dkim: &[DkimOutput], header_from: &str) {
116 for dkim in dkim {
117 self.set_dkim_result(dkim, header_from);
118 }
119 }
120
121 pub fn with_dkim_result(mut self, dkim: &DkimOutput, header_from: &str) -> Self {
124 self.set_dkim_result(dkim, header_from);
125 self
126 }
127
128 pub fn set_dkim_result(&mut self, dkim: &DkimOutput, header_from: &str) {
137 if !dkim.is_atps {
138 self.auth_results.push_str(";\r\n\tdkim=");
139 } else {
140 self.auth_results.push_str(";\r\n\tdkim-atps=");
141 }
142 dkim.result.as_auth_result(&mut self.auth_results);
143 if let Some(signature) = &dkim.signature {
144 if !signature.i.is_empty() {
145 self.auth_results.push_str(" header.i=");
146 push_quoted_pvalue(&mut self.auth_results, &signature.i);
147 } else {
148 self.auth_results.push_str(" header.d=");
149 push_pvalue(&mut self.auth_results, &signature.d);
150 }
151 self.auth_results.push_str(" header.s=");
152 push_pvalue(&mut self.auth_results, &signature.s);
153 if let Some(prefix) = signature.b.get(..6) {
154 self.auth_results.push_str(" header.b=");
155 base64::STANDARD.encode_append(prefix, &mut self.auth_results);
156 }
157 }
158
159 if dkim.is_atps {
160 self.auth_results.push_str(" header.from=");
161 push_quoted_pvalue(&mut self.auth_results, header_from);
162 }
163 }
164
165 pub fn with_dkim2_result(mut self, dkim2: &Dkim2Output) -> Self {
167 self.set_dkim2_result(dkim2);
168 self
169 }
170
171 pub fn set_dkim2_result(&mut self, dkim2: &Dkim2Output) {
178 self.auth_results.push_str(";\r\n\tdkim2=");
179 dkim2.result().as_auth_result(&mut self.auth_results);
180
181 let link = if dkim2.verification() != Verification::Full {
182 None
183 } else if matches!(dkim2.result(), Dkim2Result::Pass) {
184 dkim2.chain().first()
185 } else {
186 dkim2
187 .chain()
188 .iter()
189 .find(|link| !matches!(link.result, Dkim2Result::Pass))
190 .or_else(|| dkim2.chain().first())
191 };
192 if let Some(link) = link {
193 self.auth_results.push_str(" header.d=");
194 push_pvalue(&mut self.auth_results, &link.signature.d);
195 self.auth_results.push_str(" header.i=");
196 push_integer(&mut self.auth_results, link.signature.i as u64);
197 }
198 }
199
200 pub fn with_spf_result(mut self, spf: &SpfOutput, params: &SpfParameters<'_>) -> Self {
202 self.set_spf_result(spf, params);
203 self
204 }
205
206 pub fn set_spf_result(&mut self, spf: &SpfOutput, params: &SpfParameters<'_>) {
218 let ip_addr = params.ip();
219 let helo_domain = sanitize_pvalue(params.helo_domain());
220 self.auth_results.push_str(";\r\n\tspf=");
221 match spf_mail_from(spf, params) {
222 None => {
223 spf.result.as_spf_result(
224 &mut self.auth_results,
225 self.hostname,
226 [POSTMASTER_AT, helo_domain.as_ref()],
227 ip_addr,
228 );
229 self.auth_results.push_str(" smtp.helo=");
230 self.auth_results.push_str(helo_domain.as_ref());
231 }
232 Some(from) => {
233 let sanitized_from = sanitize_pvalue(from);
234 let mail_from = if !from.is_empty() {
235 [sanitized_from.as_ref(), ""]
236 } else {
237 [POSTMASTER_AT, helo_domain.as_ref()]
238 };
239 spf.result
240 .as_spf_result(&mut self.auth_results, self.hostname, mail_from, ip_addr);
241 self.auth_results.push_str(" smtp.mailfrom=");
242 if !from.is_empty() {
243 push_quoted_pvalue(&mut self.auth_results, from);
244 } else {
245 self.auth_results.push_str("<>");
246 }
247 }
248 }
249 }
250
251 #[cfg(feature = "arc")]
253 pub fn with_arc_result(mut self, arc: &ArcOutput, remote_ip: IpAddr) -> Self {
254 self.set_arc_result(arc, remote_ip);
255 self
256 }
257
258 #[cfg(feature = "arc")]
262 pub fn set_arc_result(&mut self, arc: &ArcOutput, remote_ip: IpAddr) {
263 self.auth_results.push_str(";\r\n\tarc=");
264 arc.result.as_auth_result(&mut self.auth_results);
265 self.auth_results.push_str(" smtp.remote-ip=");
266 push_ip_as_pvalue(&mut self.auth_results, remote_ip);
267 }
268
269 pub fn with_dmarc_result(mut self, dmarc: &DmarcOutput) -> Self {
271 self.set_dmarc_result(dmarc);
272 self
273 }
274
275 pub fn set_dmarc_result(&mut self, dmarc: &DmarcOutput) {
282 self.auth_results.push_str(";\r\n\tdmarc=");
283 match dmarc.mechanism_result() {
284 Some(result) => result.as_auth_result(&mut self.auth_results),
285 None => dmarc.result().as_auth_result(&mut self.auth_results),
286 }
287 self.auth_results.push_str(" header.from=");
288 push_pvalue(&mut self.auth_results, &dmarc.domain);
289 self.auth_results.push_str(match dmarc.policy {
290 Policy::Quarantine => " policy.dmarc=quarantine",
291 Policy::Reject => " policy.dmarc=reject",
292 Policy::None | Policy::Unspecified => " policy.dmarc=none",
293 });
294 }
295
296 pub fn with_iprev_result(mut self, iprev: &IprevOutput, remote_ip: IpAddr) -> Self {
298 self.set_iprev_result(iprev, remote_ip);
299 self
300 }
301
302 pub fn set_iprev_result(&mut self, iprev: &IprevOutput, remote_ip: IpAddr) {
306 self.auth_results.push_str(";\r\n\tiprev=");
307 iprev.result.as_auth_result(&mut self.auth_results);
308 self.auth_results.push_str(" policy.iprev=");
309 push_ip_as_pvalue(&mut self.auth_results, remote_ip);
310 }
311}
312
313impl Display for AuthenticationResults<'_> {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 f.write_str(self.hostname)?;
316 f.write_str(&self.auth_results)
317 }
318}
319
320impl HeaderWriter for AuthenticationResults<'_> {
321 fn write_header(&self, writer: &mut impl Writer) {
322 writer.write(b"Authentication-Results: ");
323 writer.write(self.hostname.as_bytes());
324 if !self.auth_results.is_empty() {
325 writer.write(self.auth_results.as_bytes());
326 } else {
327 writer.write(b"; none");
328 }
329 writer.write(b"\r\n");
330 }
331}
332
333impl HeaderWriter for ReceivedSpf {
334 fn write_header(&self, writer: &mut impl Writer) {
335 writer.write(b"Received-SPF: ");
336 writer.write(self.received_spf.as_bytes());
337 writer.write(b"\r\n");
338 }
339}
340
341fn spf_mail_from<'x>(spf: &SpfOutput, params: &SpfParameters<'x>) -> Option<&'x str> {
342 match spf.identity {
343 Some(SpfIdentity::Helo) => None,
344 Some(SpfIdentity::MailFrom) | None => params.mail_from_address(),
345 }
346}
347
348impl ReceivedSpf {
349 pub fn new(spf: &SpfOutput, params: &SpfParameters<'_>) -> Self {
361 let ip_addr = params.ip();
362 let helo = params.helo_domain();
363 let mail_from = params.mail_from_address().unwrap_or_default();
364 let hostname = params.host_domain();
365 let mut received_spf = String::with_capacity(256);
366 let helo = sanitize_pvalue(helo);
367 let checked = spf_mail_from(spf, params).unwrap_or_default();
368 let sanitized_checked = sanitize_pvalue(checked);
369 let envelope_from = if !mail_from.is_empty() {
370 [mail_from, ""]
371 } else {
372 [POSTMASTER_AT, helo.as_ref()]
373 };
374 let pieces = if !checked.is_empty() {
375 [sanitized_checked.as_ref(), ""]
376 } else {
377 [POSTMASTER_AT, helo.as_ref()]
378 };
379
380 spf.result
381 .as_spf_result(&mut received_spf, hostname, pieces, ip_addr);
382
383 received_spf.push_str("\r\n\treceiver=");
384 received_spf.push_str(hostname);
385 received_spf.push_str("; client-ip=");
386 push_ip(&mut received_spf, ip_addr);
387 received_spf.push_str("; envelope-from=\"");
388 for piece in envelope_from {
389 push_qcontent(&mut received_spf, piece);
390 }
391 received_spf.push_str("\"; helo=");
392 received_spf.push_str(helo.as_ref());
393 received_spf.push(';');
394
395 ReceivedSpf { received_spf }
396 }
397}
398
399const POSTMASTER_AT: &str = "postmaster@";
400const MAX_IP_TEXT_LEN: usize = 46;
401
402impl SpfResult {
403 fn as_spf_result(
404 &self,
405 header: &mut String,
406 hostname: &str,
407 mail_from: [&str; 2],
408 ip_addr: IpAddr,
409 ) {
410 let (result, reason, designation, close) = match self {
411 SpfResult::Pass => (
412 "pass (",
413 ": domain of ",
414 Some(" designates "),
415 " as permitted sender)",
416 ),
417 SpfResult::Fail => (
418 "fail (",
419 ": domain of ",
420 Some(" does not designate "),
421 " as permitted sender)",
422 ),
423 SpfResult::SoftFail => (
424 "softfail (",
425 ": domain of ",
426 Some(" reports soft fail for "),
427 ")",
428 ),
429 SpfResult::Neutral => (
430 "neutral (",
431 ": domain of ",
432 Some(" reports neutral for "),
433 ")",
434 ),
435 SpfResult::TempError => (
436 "temperror (",
437 ": temporary dns error validating ",
438 None,
439 ")",
440 ),
441 SpfResult::PermError => (
442 "permerror (",
443 ": unable to verify SPF record for ",
444 None,
445 ")",
446 ),
447 SpfResult::None => ("none (", ": no SPF records found for ", None, ")"),
448 };
449
450 let mail_from_len = mail_from[0].len() + mail_from[1].len();
451 header.reserve(
452 result.len()
453 + hostname.len()
454 + reason.len()
455 + mail_from_len
456 + close.len()
457 + designation.map_or(0, |text| text.len() + MAX_IP_TEXT_LEN),
458 );
459
460 header.push_str(result);
461 header.push_str(hostname);
462 header.push_str(reason);
463 for piece in mail_from {
464 header.push_str(piece);
465 }
466 if let Some(designation) = designation {
467 header.push_str(designation);
468 push_ip(header, ip_addr);
469 }
470 header.push_str(close);
471 }
472}
473
474pub(crate) trait AsAuthResult {
479 fn as_auth_result(&self, header: &mut String);
483}
484
485impl AsAuthResult for DmarcResult {
486 fn as_auth_result(&self, header: &mut String) {
487 match &self {
488 DmarcResult::Pass => header.push_str("pass"),
489 DmarcResult::Fail(err) => {
490 header.push_str("fail");
491 err.as_auth_result(header);
492 }
493 DmarcResult::PermError(err) => {
494 header.push_str("permerror");
495 err.as_auth_result(header);
496 }
497 DmarcResult::TempError(err) => {
498 header.push_str("temperror");
499 err.as_auth_result(header);
500 }
501 DmarcResult::None => header.push_str("none"),
502 }
503 }
504}
505
506impl AsAuthResult for IprevResult {
507 fn as_auth_result(&self, header: &mut String) {
508 match &self {
509 IprevResult::Pass => header.push_str("pass"),
510 IprevResult::Fail(err) => {
511 header.push_str("fail");
512 err.as_auth_result(header);
513 }
514 IprevResult::PermError(err) => {
515 header.push_str("permerror");
516 err.as_auth_result(header);
517 }
518 IprevResult::TempError(err) => {
519 header.push_str("temperror");
520 err.as_auth_result(header);
521 }
522 IprevResult::None => header.push_str("none"),
523 }
524 }
525}
526
527impl AsAuthResult for DkimResult {
528 fn as_auth_result(&self, header: &mut String) {
529 match &self {
530 DkimResult::Pass => header.push_str("pass"),
531 DkimResult::Neutral(err) => {
532 header.push_str("neutral");
533 err.as_auth_result(header);
534 }
535 DkimResult::Fail(err) => {
536 header.push_str("fail");
537 err.as_auth_result(header);
538 }
539 DkimResult::PermError(err) => {
540 header.push_str("permerror");
541 err.as_auth_result(header);
542 }
543 DkimResult::TempError(err) => {
544 header.push_str("temperror");
545 err.as_auth_result(header);
546 }
547 DkimResult::None => header.push_str("none"),
548 }
549 }
550}
551
552impl AsAuthResult for Dkim2Result {
553 fn as_auth_result(&self, header: &mut String) {
554 match &self {
555 Dkim2Result::Pass => header.push_str("pass"),
556 Dkim2Result::Fail(err) => {
557 header.push_str("fail");
558 err.as_auth_result(header);
559 }
560 Dkim2Result::PermError(err) => {
561 header.push_str("permerror");
562 err.as_auth_result(header);
563 }
564 Dkim2Result::TempError(err) => {
565 header.push_str("temperror");
566 err.as_auth_result(header);
567 }
568 Dkim2Result::None => header.push_str("none"),
569 }
570 }
571}
572
573impl AsAuthResult for Error {
574 fn as_auth_result(&self, header: &mut String) {
575 header.push_str(" (");
576 header.push_str(match self {
577 Error::Parse => "dns record parse error",
578 Error::MissingParameters => "missing parameters",
579 Error::NoHeadersFound => "no headers found",
580 Error::Crypto(CryptoError::Library(_)) => "verification failed",
581 Error::Io(_) => "i/o error",
582 Error::Base64 => "base64 error",
583 Error::Dkim(DkimError::UnsupportedAlgorithm) => "unsupported algorithm",
584 Error::Dkim(DkimError::UnsupportedCanonicalization) => "unsupported canonicalization",
585 Error::Dkim(DkimError::UnsupportedKeyType) => "unsupported key type",
586 Error::Crypto(CryptoError::FailedVerification) => "verification failed",
587 Error::Crypto(CryptoError::IncompatibleAlgorithms) => {
588 "incompatible record/signature algorithms"
589 }
590 Error::Dns(DnsError::Resolver(_)) => "dns error",
591 Error::Dns(DnsError::RecordNotFound(_)) => "dns record not found",
592 Error::Dkim(DkimError::UnsupportedVersion) => "unsupported version",
593 Error::Dkim(DkimError::BodyHashMismatch) => "body hash did not verify",
594 #[cfg(feature = "arc")]
595 Error::Arc(ArcError::BodyHashMismatch) => "body hash did not verify",
596 Error::Dkim(DkimError::AuidMismatch) => "auid does not match",
597 Error::Dkim(DkimError::PublicKeyRevoked) => "revoked public key",
598 Error::Dkim(DkimError::SignatureExpired) => "signature error",
599 #[cfg(feature = "arc")]
600 Error::Arc(ArcError::SignatureExpired) => "signature error",
601 Error::Dkim(DkimError::BodyLengthTag) => {
602 "signature length ignored due to security risk"
603 }
604 #[cfg(feature = "arc")]
605 Error::Arc(ArcError::BodyLengthTag) => "signature length ignored due to security risk",
606 #[cfg(feature = "arc")]
607 Error::Arc(ArcError::InvalidInstance(i)) => {
608 write!(header, "invalid ARC instance {i})").ok();
609 return;
610 }
611 #[cfg(feature = "arc")]
612 Error::Arc(ArcError::InvalidChainValidation) => "invalid ARC cv",
613 #[cfg(feature = "arc")]
614 Error::Arc(ArcError::ChainTooLong) => "too many ARC headers",
615 #[cfg(feature = "arc")]
616 Error::Arc(ArcError::HasHeaderTag) => "ARC has header tag",
617 #[cfg(feature = "arc")]
618 Error::Arc(ArcError::BrokenChain) => "broken ARC chain",
619 Error::NotAligned => "policy not aligned",
620 Error::Dns(DnsError::InvalidRecordType) => "invalid dns record type",
621 Error::Dkim2(e) => {
622 write!(CommentWriter(header), "{e}").ok();
623 header.push(')');
624 return;
625 }
626 });
627 header.push(')');
628 }
629}
630
631struct CommentWriter<'x>(&'x mut String);
632
633impl Write for CommentWriter<'_> {
634 fn write_str(&mut self, value: &str) -> std::fmt::Result {
635 for ch in value.chars() {
636 match ch {
637 '(' | ')' | '\\' => {
638 self.0.push('\\');
639 self.0.push(ch);
640 }
641 '\0'..='\u{1f}' | '\u{7f}' => {}
642 _ => self.0.push(ch),
643 }
644 }
645 Ok(())
646 }
647}
648
649fn push_ip_as_pvalue(header: &mut String, ip: IpAddr) {
656 match ip {
657 IpAddr::V4(addr) => push_ipv4(header, addr),
658 IpAddr::V6(addr) => {
659 header.push('"');
660 write!(header, "{addr}").ok();
661 header.push('"');
662 }
663 }
664}
665
666fn push_ip(header: &mut String, ip: IpAddr) {
667 match ip {
668 IpAddr::V4(addr) => push_ipv4(header, addr),
669 IpAddr::V6(addr) => {
670 write!(header, "{addr}").ok();
671 }
672 }
673}
674
675fn push_ipv4(header: &mut String, addr: Ipv4Addr) {
676 const MAX_IPV4_TEXT_LEN: usize = 15;
677
678 let mut text = [0u8; MAX_IPV4_TEXT_LEN];
679 let mut len = 0;
680 let mut push = |byte: u8| {
681 if let Some(slot) = text.get_mut(len) {
682 *slot = byte;
683 len += 1;
684 }
685 };
686
687 for (pos, octet) in addr.octets().into_iter().enumerate() {
688 if pos > 0 {
689 push(b'.');
690 }
691 if octet >= 100 {
692 push(b'0' + octet / 100);
693 }
694 if octet >= 10 {
695 push(b'0' + (octet / 10) % 10);
696 }
697 push(b'0' + octet % 10);
698 }
699
700 header.push_str(std::str::from_utf8(text.get(..len).unwrap_or_default()).unwrap_or_default());
701}
702
703fn push_integer(header: &mut String, value: u64) {
704 let mut integer = IntegerBuffer::new();
705 header.push_str(integer.text(value));
706}
707
708#[inline]
709fn is_pvalue_safe(ch: char) -> bool {
710 !matches!(ch, '\0'..=' ' | '\u{7f}'..='\u{9f}' | '(' | ')' | ';' | '=' | '"' | '\\')
711}
712
713#[inline(always)]
714fn is_pvalue_safe_ascii(ch: u8) -> bool {
715 !matches!(ch, 0..=b' ' | 0x7f..=u8::MAX | b'(' | b')' | b';' | b'=' | b'"' | b'\\')
716}
717
718#[inline]
719fn is_pvalue_clean(value: &str) -> bool {
720 value.bytes().all(is_pvalue_safe_ascii) || value.chars().all(is_pvalue_safe)
721}
722
723#[inline]
724fn sanitize_pvalue(value: &str) -> Cow<'_, str> {
725 if is_pvalue_clean(value) {
726 Cow::Borrowed(value)
727 } else {
728 Cow::Owned(value.chars().filter(|&ch| is_pvalue_safe(ch)).collect())
729 }
730}
731
732#[inline]
733fn push_pvalue(header: &mut String, value: &str) {
734 if is_pvalue_clean(value) {
735 header.push_str(value);
736 } else {
737 header.extend(value.chars().filter(|&ch| is_pvalue_safe(ch)));
738 }
739}
740
741#[inline]
742fn push_quoted_pvalue(header: &mut String, value: &str) {
743 if !value.is_empty() && is_pvalue_clean(value) {
744 header.push_str(value);
745 } else {
746 header.push('"');
747 push_qcontent(header, value);
748 header.push('"');
749 }
750}
751
752#[inline]
753fn push_qcontent(header: &mut String, value: &str) {
754 let mut start = 0;
755 for (pos, ch) in value.char_indices() {
756 match ch {
757 '"' | '\\' => {
758 header.push_str(value.get(start..pos).unwrap_or_default());
759 header.push('\\');
760 header.push(ch);
761 start = pos + 1;
762 }
763 '\0'..='\u{1f}' | '\u{7f}'..='\u{9f}' => {
764 header.push_str(value.get(start..pos).unwrap_or_default());
765 start = pos + ch.len_utf8();
766 }
767 _ => {}
768 }
769 }
770 header.push_str(value.get(start..).unwrap_or_default());
771}
772
773#[cfg(test)]
774mod tests;