1use std::fmt::Write;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct AuthResult {
17 pub method: String,
19 pub result: String,
22 pub reason: Option<String>,
24}
25
26pub fn format_auth_results(hostname: &str, results: &[AuthResult]) -> String {
35 let mut buf = String::new();
36 write!(buf, "{hostname}").unwrap();
37
38 if results.is_empty() {
39 buf.push_str("; none");
40 return buf;
41 }
42
43 for r in results {
44 write!(buf, ";\r\n\t{}={}", r.method, r.result).unwrap();
45 if let Some(ref reason) = r.reason {
46 write!(buf, " reason=\"{reason}\"").unwrap();
47 }
48 }
49 buf
50}
51
52pub fn format_auth_results_header(hostname: &str, results: &[AuthResult]) -> String {
54 format!(
55 "Authentication-Results: {}\r\n",
56 format_auth_results(hostname, results)
57 )
58}
59
60pub fn build_auth_header(
67 hostname: &str,
68 spf: &str,
69 dkim: &str,
70 arc: &str,
71 dmarc: &str,
72 dmarc_reason: Option<&str>,
73) -> String {
74 let results = vec![
75 AuthResult {
76 method: "spf".into(),
77 result: spf.into(),
78 reason: None,
79 },
80 AuthResult {
81 method: "dkim".into(),
82 result: dkim.into(),
83 reason: None,
84 },
85 AuthResult {
86 method: "arc".into(),
87 result: arc.into(),
88 reason: None,
89 },
90 AuthResult {
91 method: "dmarc".into(),
92 result: dmarc.into(),
93 reason: dmarc_reason.map(|s| s.to_string()),
94 },
95 ];
96 format_auth_results_header(hostname, &results)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn all_pass() {
105 let results = vec![
106 AuthResult {
107 method: "spf".into(),
108 result: "pass".into(),
109 reason: None,
110 },
111 AuthResult {
112 method: "dkim".into(),
113 result: "pass".into(),
114 reason: None,
115 },
116 AuthResult {
117 method: "dmarc".into(),
118 result: "pass".into(),
119 reason: None,
120 },
121 ];
122 let header = format_auth_results("mx.example.com", &results);
123 assert!(header.starts_with("mx.example.com;"));
124 assert!(header.contains("spf=pass"));
125 assert!(header.contains("dkim=pass"));
126 assert!(header.contains("dmarc=pass"));
127 }
128
129 #[test]
130 fn spf_fail_with_reason() {
131 let results = vec![AuthResult {
132 method: "spf".into(),
133 result: "fail".into(),
134 reason: Some("mechanism -all matched".into()),
135 }];
136 let header = format_auth_results("mx.example.com", &results);
137 assert!(header.contains("spf=fail"));
138 assert!(header.contains("reason=\"mechanism -all matched\""));
139 }
140
141 #[test]
142 fn no_results_yields_none() {
143 let header = format_auth_results("mx.example.com", &[]);
144 assert_eq!(header, "mx.example.com; none");
145 }
146
147 #[test]
148 fn full_header_starts_and_ends_correctly() {
149 let results = vec![AuthResult {
150 method: "spf".into(),
151 result: "pass".into(),
152 reason: None,
153 }];
154 let header = format_auth_results_header("mx.example.com", &results);
155 assert!(header.starts_with("Authentication-Results: mx.example.com;"));
156 assert!(header.ends_with("\r\n"));
157 }
158
159 #[test]
160 fn dmarc_policy_reason_round_trips() {
161 let results = vec![AuthResult {
162 method: "dmarc".into(),
163 result: "fail".into(),
164 reason: Some("policy=quarantine".into()),
165 }];
166 let header = format_auth_results("mx.example.com", &results);
167 assert!(header.contains("reason=\"policy=quarantine\""));
168 }
169
170 #[test]
171 fn full_pipeline_quadruple() {
172 let results = vec![
173 AuthResult { method: "spf".into(), result: "pass".into(), reason: None },
174 AuthResult { method: "dkim".into(), result: "pass".into(), reason: None },
175 AuthResult { method: "arc".into(), result: "none".into(), reason: None },
176 AuthResult { method: "dmarc".into(), result: "pass".into(), reason: None },
177 ];
178 let header = format_auth_results("mx.mail.com", &results);
179 assert!(header.contains("spf=pass"));
180 assert!(header.contains("dkim=pass"));
181 assert!(header.contains("arc=none"));
182 assert!(header.contains("dmarc=pass"));
183 }
184
185 #[test]
186 fn multiline_folding() {
187 let results = vec![
188 AuthResult { method: "spf".into(), result: "pass".into(), reason: None },
189 AuthResult { method: "dmarc".into(), result: "pass".into(), reason: None },
190 ];
191 let header = format_auth_results("mx.example.com", &results);
192 assert!(header.contains(";\r\n\t"));
194 }
195
196 #[test]
197 fn temperror_and_permerror_results_pass_through() {
198 for code in &["temperror", "permerror"] {
199 let results = vec![AuthResult {
200 method: "dmarc".into(),
201 result: (*code).into(),
202 reason: None,
203 }];
204 let header = format_auth_results("mx.example.com", &results);
205 assert!(header.contains(&format!("dmarc={code}")));
206 }
207 }
208
209 #[test]
210 fn build_auth_header_canonical_quadruple() {
211 let header = build_auth_header("mx.test.com", "pass", "pass", "none", "pass", None);
212 assert!(header.contains("Authentication-Results: mx.test.com"));
213 assert!(header.contains("spf=pass"));
214 assert!(header.contains("dkim=pass"));
215 assert!(header.contains("arc=none"));
216 assert!(header.contains("dmarc=pass"));
217 }
218
219 #[test]
220 fn build_auth_header_threads_dmarc_reason() {
221 let header = build_auth_header(
222 "mx.test.com", "pass", "fail", "none", "fail", Some("policy=reject"),
223 );
224 assert!(header.contains("dmarc=fail"));
225 assert!(header.contains("reason=\"policy=reject\""));
226 }
227
228 #[test]
229 fn build_auth_header_omits_dmarc_reason_when_none() {
230 let header = build_auth_header(
231 "mx.test.com", "pass", "pass", "none", "pass", None,
232 );
233 assert!(!header.contains("reason="));
234 }
235}