Commit
+59 -274 +/-5 browse
1 | diff --git a/Cargo.toml b/Cargo.toml |
2 | index fdb69c1..b21deeb 100644 |
3 | --- a/Cargo.toml |
4 | +++ b/Cargo.toml |
5 | @@ -1,7 +1,7 @@ |
6 | [package] |
7 | name = "mail-auth" |
8 | description = "DKIM, ARC, SPF and DMARC library for Rust" |
9 | - version = "0.4.3" |
10 | + version = "0.5.0" |
11 | edition = "2021" |
12 | authors = [ "Stalwart Labs <hello@stalw.art>"] |
13 | license = "Apache-2.0 OR MIT" |
14 | @@ -29,7 +29,7 @@ lru-cache = "0.1.2" |
15 | mail-parser = { version = "0.9", features = ["ludicrous_mode", "full_encoding"] } |
16 | mail-builder = { version = "0.3", features = ["ludicrous_mode"] } |
17 | parking_lot = "0.12.0" |
18 | - quick-xml = "0.34" |
19 | + quick-xml = "0.36" |
20 | ring = { version = "0.17", optional = true } |
21 | rsa = { version = "0.9.6", optional = true } |
22 | rustls-pemfile = { version = "2", optional = true } |
23 | @@ -44,3 +44,4 @@ rand = { version = "0.8.5", optional = true } |
24 | [dev-dependencies] |
25 | tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] } |
26 | rustls-pemfile = "2" |
27 | + psl = "2.1.55" |
28 | diff --git a/README.md b/README.md |
29 | index 28087f5..23b1d9a 100644 |
30 | --- a/README.md |
31 | +++ b/README.md |
32 | @@ -175,6 +175,7 @@ Features: |
33 | &dkim_result, |
34 | "example.org", |
35 | &spf_result, |
36 | + |domain| psl::domain_str(domain).unwrap_or(domain), |
37 | ) |
38 | .await; |
39 | assert_eq!(dmarc_result.dkim_result(), &DmarcResult::Pass); |
40 | diff --git a/examples/dmarc_verify.rs b/examples/dmarc_verify.rs |
41 | index 85815b1..c172851 100644 |
42 | --- a/examples/dmarc_verify.rs |
43 | +++ b/examples/dmarc_verify.rs |
44 | @@ -63,6 +63,7 @@ async fn main() { |
45 | &dkim_result, |
46 | "example.org", |
47 | &spf_result, |
48 | + |domain| psl::domain_str(domain).unwrap_or(domain), |
49 | ) |
50 | .await; |
51 | assert_eq!(dmarc_result.dkim_result(), &DmarcResult::Pass); |
52 | diff --git a/src/dmarc/verify.rs b/src/dmarc/verify.rs |
53 | index 630947c..6377716 100644 |
54 | --- a/src/dmarc/verify.rs |
55 | +++ b/src/dmarc/verify.rs |
56 | @@ -18,39 +18,40 @@ use crate::{ |
57 | use super::{Alignment, Dmarc, URI}; |
58 | |
59 | impl Resolver { |
60 | - /// Verifies the DMARC policy of an RFC5322.From domain |
61 | + /// Verifies the DMARC policy of an RFC5321.MailFrom domain |
62 | pub async fn verify_dmarc( |
63 | &self, |
64 | message: &AuthenticatedMessage<'_>, |
65 | dkim_output: &[DkimOutput<'_>], |
66 | - mail_from_domain: &str, |
67 | + rfc5321_mail_from_domain: &str, |
68 | spf_output: &SpfOutput, |
69 | + domain_suffix_fn: impl Fn(&str) -> &str, |
70 | ) -> DmarcOutput { |
71 | - // Extract RFC5322.From |
72 | - let mut from_domain = ""; |
73 | + // Extract RFC5322.From domain |
74 | + let mut rfc5322_from_domain = ""; |
75 | for from in &message.from { |
76 | if let Some((_, domain)) = from.rsplit_once('@') { |
77 | - if from_domain.is_empty() { |
78 | - from_domain = domain; |
79 | - } else if from_domain != domain { |
80 | + if rfc5322_from_domain.is_empty() { |
81 | + rfc5322_from_domain = domain; |
82 | + } else if rfc5322_from_domain != domain { |
83 | // Multi-valued RFC5322.From header fields with multiple |
84 | // domains MUST be exempt from DMARC checking. |
85 | return DmarcOutput::default(); |
86 | } |
87 | } |
88 | } |
89 | - if from_domain.is_empty() { |
90 | + if rfc5322_from_domain.is_empty() { |
91 | return DmarcOutput::default(); |
92 | } |
93 | |
94 | // Obtain DMARC policy |
95 | - let dmarc = match self.dmarc_tree_walk(from_domain).await { |
96 | + let dmarc = match self.dmarc_tree_walk(rfc5322_from_domain).await { |
97 | Ok(Some(dmarc)) => dmarc, |
98 | - Ok(None) => return DmarcOutput::default().with_domain(from_domain), |
99 | + Ok(None) => return DmarcOutput::default().with_domain(rfc5322_from_domain), |
100 | Err(err) => { |
101 | let err = DmarcResult::from(err); |
102 | return DmarcOutput::default() |
103 | - .with_domain(from_domain) |
104 | + .with_domain(rfc5322_from_domain) |
105 | .with_dkim_result(err.clone()) |
106 | .with_spf_result(err); |
107 | } |
108 | @@ -59,7 +60,7 @@ impl Resolver { |
109 | let mut output = DmarcOutput { |
110 | spf_result: DmarcResult::None, |
111 | dkim_result: DmarcResult::None, |
112 | - domain: from_domain.to_string(), |
113 | + domain: rfc5322_from_domain.to_string(), |
114 | policy: dmarc.p, |
115 | record: None, |
116 | }; |
117 | @@ -67,13 +68,14 @@ impl Resolver { |
118 | let has_dkim_pass = dkim_output.iter().any(|o| o.result == DkimResult::Pass); |
119 | if spf_output.result == SpfResult::Pass || has_dkim_pass { |
120 | // Check SPF alignment |
121 | - let from_subdomain = format!(".{from_domain}"); |
122 | + let from_subdomain = format!(".{}", domain_suffix_fn(rfc5322_from_domain)); |
123 | if spf_output.result == SpfResult::Pass { |
124 | - output.spf_result = if mail_from_domain == from_domain { |
125 | + output.spf_result = if rfc5321_mail_from_domain == rfc5322_from_domain { |
126 | DmarcResult::Pass |
127 | } else if dmarc.aspf == Alignment::Relaxed |
128 | - && mail_from_domain.ends_with(&from_subdomain) |
129 | - || from_domain.ends_with(&format!(".{mail_from_domain}")) |
130 | + && rfc5321_mail_from_domain.ends_with(&from_subdomain) |
131 | + || rfc5322_from_domain |
132 | + .ends_with(&format!(".{}", domain_suffix_fn(rfc5321_mail_from_domain))) |
133 | { |
134 | output.policy = dmarc.sp; |
135 | DmarcResult::Pass |
136 | @@ -85,15 +87,18 @@ impl Resolver { |
137 | // Check DKIM alignment |
138 | if has_dkim_pass { |
139 | output.dkim_result = if dkim_output.iter().any(|o| { |
140 | - o.result == DkimResult::Pass && o.signature.as_ref().unwrap().d.eq(from_domain) |
141 | + o.result == DkimResult::Pass |
142 | + && o.signature.as_ref().unwrap().d.eq(rfc5322_from_domain) |
143 | }) { |
144 | DmarcResult::Pass |
145 | } else if dmarc.adkim == Alignment::Relaxed |
146 | && dkim_output.iter().any(|o| { |
147 | o.result == DkimResult::Pass |
148 | && (o.signature.as_ref().unwrap().d.ends_with(&from_subdomain) |
149 | - || from_domain |
150 | - .ends_with(&format!(".{}", o.signature.as_ref().unwrap().d))) |
151 | + || rfc5322_from_domain.ends_with(&format!( |
152 | + ".{}", |
153 | + domain_suffix_fn(&o.signature.as_ref().unwrap().d) |
154 | + ))) |
155 | }) |
156 | { |
157 | output.policy = dmarc.sp; |
158 | @@ -102,8 +107,10 @@ impl Resolver { |
159 | if dkim_output.iter().any(|o| { |
160 | o.result == DkimResult::Pass |
161 | && (o.signature.as_ref().unwrap().d.ends_with(&from_subdomain) |
162 | - || from_domain |
163 | - .ends_with(&format!(".{}", o.signature.as_ref().unwrap().d))) |
164 | + || rfc5322_from_domain.ends_with(&format!( |
165 | + ".{}", |
166 | + domain_suffix_fn(&o.signature.as_ref().unwrap().d) |
167 | + ))) |
168 | }) { |
169 | output.policy = dmarc.sp; |
170 | } |
171 | @@ -208,7 +215,7 @@ mod test { |
172 | dmarc_dns, |
173 | dmarc, |
174 | message, |
175 | - mail_from_domain, |
176 | + rfc5321_mail_from_domain, |
177 | signature_domain, |
178 | dkim, |
179 | spf, |
180 | @@ -296,6 +303,22 @@ mod test { |
181 | DmarcResult::Pass, |
182 | Policy::Quarantine, |
183 | ), |
184 | + // Relaxed - Pass with tree walk and different subdomains |
185 | + ( |
186 | + "_dmarc.c.example.org.", |
187 | + concat!( |
188 | + "v=DMARC1; p=reject; sp=quarantine; np=None; aspf=r; adkim=r; fo=1;", |
189 | + "rua=mailto:dmarc-feedback@example.org" |
190 | + ), |
191 | + "From: hello@a.b.c.example.org\r\n\r\n", |
192 | + "z.example.org", |
193 | + "z.example.org", |
194 | + DkimResult::Pass, |
195 | + SpfResult::Pass, |
196 | + DmarcResult::Pass, |
197 | + DmarcResult::Pass, |
198 | + Policy::Quarantine, |
199 | + ), |
200 | // Failed mechanisms |
201 | ( |
202 | "_dmarc.example.org.", |
203 | @@ -333,12 +356,18 @@ mod test { |
204 | }; |
205 | let spf = SpfOutput { |
206 | result: spf, |
207 | - domain: mail_from_domain.to_string(), |
208 | + domain: rfc5321_mail_from_domain.to_string(), |
209 | report: None, |
210 | explanation: None, |
211 | }; |
212 | let result = resolver |
213 | - .verify_dmarc(&auth_message, &[dkim], mail_from_domain, &spf) |
214 | + .verify_dmarc( |
215 | + &auth_message, |
216 | + &[dkim], |
217 | + rfc5321_mail_from_domain, |
218 | + &spf, |
219 | + |d| psl::domain_str(d).unwrap_or(d), |
220 | + ) |
221 | .await; |
222 | assert_eq!(result.dkim_result, expect_dkim); |
223 | assert_eq!(result.spf_result, expect_spf); |
224 | diff --git a/src/lib.rs b/src/lib.rs |
225 | index 56f6759..67ad4b6 100644 |
226 | --- a/src/lib.rs |
227 | +++ b/src/lib.rs |
228 | @@ -8,254 +8,7 @@ |
229 | * except according to those terms. |
230 | */ |
231 | |
232 | - //! # mail-auth |
233 | - //! |
234 | - //! [![crates.io](https://img.shields.io/crates/v/mail-auth)](https://crates.io/crates/mail-auth) |
235 | - //! [![build](https://github.com/stalwartlabs/mail-auth/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/mail-auth/actions/workflows/rust.yml) |
236 | - //! [![docs.rs](https://img.shields.io/docsrs/mail-auth)](https://docs.rs/mail-auth) |
237 | - //! [![crates.io](https://img.shields.io/crates/l/mail-auth)](http://www.apache.org/licenses/LICENSE-2.0) |
238 | - //! |
239 | - //! _mail-auth_ is an e-mail authentication and reporting library written in Rust that supports the **DKIM**, **ARC**, **SPF** and **DMARC** |
240 | - //! protocols. The library aims to be fast, safe and correct while supporting all major [message authentication and reporting RFCs](#conformed-rfcs). |
241 | - //! |
242 | - //! Features: |
243 | - //! |
244 | - //! - **DomainKeys Identified Mail (DKIM)**: |
245 | - //! - ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 signing and verification. |
246 | - //! - DKIM Authorized Third-Party Signatures. |
247 | - //! - DKIM failure reporting using the Abuse Reporting Format. |
248 | - //! - **Authenticated Received Chain (ARC)**: |
249 | - //! - ED25519-SHA256 (Edwards-Curve Digital Signature Algorithm), RSA-SHA256 and RSA-SHA1 chain verification. |
250 | - //! - ARC sealing. |
251 | - //! - **Sender Policy Framework (SPF)**: |
252 | - //! - Policy evaluation. |
253 | - //! - SPF failure reporting using the Abuse Reporting Format. |
254 | - //! - **Domain-based Message Authentication, Reporting, and Conformance (DMARC)**: |
255 | - //! - Policy evaluation. |
256 | - //! - DMARC aggregate report parsing and generation. |
257 | - //! - **Abuse Reporting Format (ARF)**: |
258 | - //! - Abuse and Authentication failure reporting. |
259 | - //! - Feedback report parsing and generation. |
260 | - //! - **SMTP TLS Reporting**: |
261 | - //! - Report parsing and generation. |
262 | - //! |
263 | - //! ## Usage examples |
264 | - //! |
265 | - //! ### DKIM Signature Verification |
266 | - //! |
267 | - //! ```rust |
268 | - //! // Create a resolver using Cloudflare DNS |
269 | - //! let resolver = Resolver::new_cloudflare_tls().unwrap(); |
270 | - //! |
271 | - //! // Parse message |
272 | - //! let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap(); |
273 | - //! |
274 | - //! // Validate signature |
275 | - //! let result = resolver.verify_dkim(&authenticated_message).await; |
276 | - //! |
277 | - //! // Make sure all signatures passed verification |
278 | - //! assert!(result.iter().all(|s| s.result() == &DkimResult::Pass)); |
279 | - //! ``` |
280 | - //! |
281 | - //! ### DKIM Signing |
282 | - //! |
283 | - //! ```rust |
284 | - //! // Sign an e-mail message using RSA-SHA256 |
285 | - //! let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap(); |
286 | - //! let signature_rsa = DkimSigner::from_key(pk_rsa) |
287 | - //! .domain("example.com") |
288 | - //! .selector("default") |
289 | - //! .headers(["From", "To", "Subject"]) |
290 | - //! .sign(RFC5322_MESSAGE.as_bytes()) |
291 | - //! .unwrap(); |
292 | - //! |
293 | - //! // Sign an e-mail message using ED25519-SHA256 |
294 | - //! let pk_ed = Ed25519Key::from_bytes( |
295 | - //! &base64_decode(ED25519_PUBLIC_KEY.as_bytes()).unwrap(), |
296 | - //! &base64_decode(ED25519_PRIVATE_KEY.as_bytes()).unwrap(), |
297 | - //! ) |
298 | - //! .unwrap(); |
299 | - //! |
300 | - //! let signature_ed = DkimSigner::from_key(pk_ed) |
301 | - //! .domain("example.com") |
302 | - //! .selector("default-ed") |
303 | - //! .headers(["From", "To", "Subject"]) |
304 | - //! .sign(RFC5322_MESSAGE.as_bytes()) |
305 | - //! .unwrap(); |
306 | - //! |
307 | - //! // Print the message including both signatures to stdout |
308 | - //! println!( |
309 | - //! "{}{}{}", |
310 | - //! signature_rsa.to_header(), |
311 | - //! signature_ed.to_header(), |
312 | - //! RFC5322_MESSAGE |
313 | - //! ); |
314 | - //! ``` |
315 | - //! |
316 | - //! ### ARC Chain Verification |
317 | - //! |
318 | - //! ```rust |
319 | - //! // Create a resolver using Cloudflare DNS |
320 | - //! let resolver = Resolver::new_cloudflare_tls().unwrap(); |
321 | - //! |
322 | - //! // Parse message |
323 | - //! let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap(); |
324 | - //! |
325 | - //! // Validate ARC chain |
326 | - //! let result = resolver.verify_arc(&authenticated_message).await; |
327 | - //! |
328 | - //! // Make sure ARC passed verification |
329 | - //! assert_eq!(result.result(), &DkimResult::Pass); |
330 | - //! ``` |
331 | - //! |
332 | - //! ### ARC Chain Sealing |
333 | - //! |
334 | - //! ```rust |
335 | - //! // Create a resolver using Cloudflare DNS |
336 | - //! let resolver = Resolver::new_cloudflare_tls().unwrap(); |
337 | - //! |
338 | - //! // Parse message to be sealed |
339 | - //! let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap(); |
340 | - //! |
341 | - //! // Verify ARC and DKIM signatures |
342 | - //! let arc_result = resolver.verify_arc(&authenticated_message).await; |
343 | - //! let dkim_result = resolver.verify_dkim(&authenticated_message).await; |
344 | - //! |
345 | - //! // Build Authenticated-Results header |
346 | - //! let auth_results = AuthenticationResults::new("mx.mydomain.org") |
347 | - //! .with_dkim_result(&dkim_result, "sender@example.org") |
348 | - //! .with_arc_result(&arc_result, "127.0.0.1".parse().unwrap()); |
349 | - //! |
350 | - //! // Seal message |
351 | - //! if arc_result.can_be_sealed() { |
352 | - //! // Seal the e-mail message using RSA-SHA256 |
353 | - //! let pk_rsa = RsaKey::<Sha256>::from_pkcs1_pem(RSA_PRIVATE_KEY).unwrap(); |
354 | - //! let arc_set = ArcSealer::from_key(pk_rsa) |
355 | - //! .domain("example.org") |
356 | - //! .selector("default") |
357 | - //! .headers(["From", "To", "Subject", "DKIM-Signature"]) |
358 | - //! .seal(&authenticated_message, &auth_results, &arc_result) |
359 | - //! .unwrap(); |
360 | - //! |
361 | - //! // Print the sealed message to stdout |
362 | - //! println!("{}{}", arc_set.to_header(), RFC5322_MESSAGE) |
363 | - //! } else { |
364 | - //! eprintln!("The message could not be sealed, probably an ARC chain with cv=fail was found.") |
365 | - //! } |
366 | - //! ``` |
367 | - //! |
368 | - //! ### SPF Policy Evaluation |
369 | - //! |
370 | - //! ```rust |
371 | - //! // Create a resolver using Cloudflare DNS |
372 | - //! let resolver = Resolver::new_cloudflare_tls().unwrap(); |
373 | - //! |
374 | - //! // Verify HELO identity |
375 | - //! let result = resolver |
376 | - //! .verify_spf_helo("127.0.0.1".parse().unwrap(), "gmail.com", "my-local-domain.org") |
377 | - //! .await; |
378 | - //! assert_eq!(result.result(), SpfResult::Fail); |
379 | - //! |
380 | - //! // Verify MAIL-FROM identity |
381 | - //! let result = resolver |
382 | - //! .verify_spf_sender("::1".parse().unwrap(), "gmail.com", "my-local-domain.org", "sender@gmail.com") |
383 | - //! .await; |
384 | - //! assert_eq!(result.result(), SpfResult::Fail); |
385 | - //! ``` |
386 | - //! |
387 | - //! ### DMARC Policy Evaluation |
388 | - //! |
389 | - //! ```rust |
390 | - //! // Create a resolver using Cloudflare DNS |
391 | - //! let resolver = Resolver::new_cloudflare_tls().unwrap(); |
392 | - //! |
393 | - //! // Verify DKIM signatures |
394 | - //! let authenticated_message = AuthenticatedMessage::parse(RFC5322_MESSAGE.as_bytes()).unwrap(); |
395 | - //! let dkim_result = resolver.verify_dkim(&authenticated_message).await; |
396 | - //! |
397 | - //! // Verify SPF MAIL-FROM identity |
398 | - //! let spf_result = resolver |
399 | - //! .verify_spf_sender("::1".parse().unwrap(), "example.org", "my-local-domain.org", "sender@example.org") |
400 | - //! .await; |
401 | - //! |
402 | - //! // Verify DMARC |
403 | - //! let dmarc_result = resolver |
404 | - //! .verify_dmarc( |
405 | - //! &authenticated_message, |
406 | - //! &dkim_result, |
407 | - //! "example.org", |
408 | - //! &spf_result, |
409 | - //! ) |
410 | - //! .await; |
411 | - //! assert_eq!(dmarc_result.dkim_result(), &DmarcResult::Pass); |
412 | - //! assert_eq!(dmarc_result.spf_result(), &DmarcResult::Pass); |
413 | - //! ``` |
414 | - //! |
415 | - //! More examples available under the [examples](examples) directory. |
416 | - //! |
417 | - //! ## Testing & Fuzzing |
418 | - //! |
419 | - //! To run the testsuite: |
420 | - //! |
421 | - //! ```bash |
422 | - //! $ cargo test --features test |
423 | - //! ``` |
424 | - //! |
425 | - //! To fuzz the library with `cargo-fuzz`: |
426 | - //! |
427 | - //! ```bash |
428 | - //! $ cargo +nightly fuzz run mail_auth |
429 | - //! ``` |
430 | - //! |
431 | - //! ## Conformed RFCs |
432 | - //! |
433 | - //! ### DKIM |
434 | - //! |
435 | - //! - [RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures](https://datatracker.ietf.org/doc/html/rfc6376) |
436 | - //! - [RFC 6541 - DomainKeys Identified Mail (DKIM) Authorized Third-Party Signatures](https://datatracker.ietf.org/doc/html/rfc6541) |
437 | - //! - [RFC 6651 - Extensions to DomainKeys Identified Mail (DKIM) for Failure Reporting](https://datatracker.ietf.org/doc/html/rfc6651) |
438 | - //! - [RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA)](https://datatracker.ietf.org/doc/html/rfc8032) |
439 | - //! - [RFC 4686 - Analysis of Threats Motivating DomainKeys Identified Mail (DKIM)](https://datatracker.ietf.org/doc/html/rfc4686) |
440 | - //! - [RFC 5016 - Requirements for a DomainKeys Identified Mail (DKIM) Signing Practices Protocol](https://datatracker.ietf.org/doc/html/rfc5016) |
441 | - //! - [RFC 5585 - DomainKeys Identified Mail (DKIM) Service Overview](https://datatracker.ietf.org/doc/html/rfc5585) |
442 | - //! - [RFC 5672 - DomainKeys Identified Mail (DKIM) Signatures -- Update](https://datatracker.ietf.org/doc/html/rfc5672) |
443 | - //! - [RFC 5863 - DomainKeys Identified Mail (DKIM) Development, Deployment, and Operations](https://datatracker.ietf.org/doc/html/rfc5863) |
444 | - //! - [RFC 6377 - DomainKeys Identified Mail (DKIM) and Mailing Lists](https://datatracker.ietf.org/doc/html/rfc6377) |
445 | - //! |
446 | - //! ### SPF |
447 | - //! - [RFC 7208 - Sender Policy Framework (SPF)](https://datatracker.ietf.org/doc/html/rfc7208) |
448 | - //! - [RFC 6652 - Sender Policy Framework (SPF) Authentication Failure Reporting Using the Abuse Reporting Format](https://datatracker.ietf.org/doc/html/rfc6652) |
449 | - //! |
450 | - //! ### DMARC |
451 | - //! - [RFC 7489 - Domain-based Message Authentication, Reporting, and Conformance (DMARC)](https://datatracker.ietf.org/doc/html/rfc7489) |
452 | - //! - [RFC 8617 - The Authenticated Received Chain (ARC) Protocol](https://datatracker.ietf.org/doc/html/rfc8617) |
453 | - //! - [RFC 8601 - Message Header Field for Indicating Message Authentication Status](https://datatracker.ietf.org/doc/html/rfc8601) |
454 | - //! - [RFC 8616 - Email Authentication for Internationalized Mail](https://datatracker.ietf.org/doc/html/rfc8616) |
455 | - //! - [RFC 7960 - Interoperability Issues between Domain-based Message Authentication, Reporting, and Conformance (DMARC) and Indirect Email Flows](https://datatracker.ietf.org/doc/html/rfc7960) |
456 | - //! |
457 | - //! ### ARF |
458 | - //! - [RFC 5965 - An Extensible Format for Email Feedback Reports](https://datatracker.ietf.org/doc/html/rfc5965) |
459 | - //! - [RFC 6430 - Email Feedback Report Type Value: not-spam](https://datatracker.ietf.org/doc/html/rfc6430) |
460 | - //! - [RFC 6590 - Redaction of Potentially Sensitive Data from Mail Abuse Reports](https://datatracker.ietf.org/doc/html/rfc6590) |
461 | - //! - [RFC 6591 - Authentication Failure Reporting Using the Abuse Reporting Format](https://datatracker.ietf.org/doc/html/rfc6591) |
462 | - //! - [RFC 6650 - Creation and Use of Email Feedback Reports: An Applicability Statement for the Abuse Reporting Format (ARF)](https://datatracker.ietf.org/doc/html/rfc6650) |
463 | - //! |
464 | - //! ### SMTP TLS Reporting |
465 | - //! - [RFC 8460 - SMTP TLS Reporting](https://datatracker.ietf.org/doc/html/rfc8460) |
466 | - //! |
467 | - //! ## License |
468 | - //! |
469 | - //! Licensed under either of |
470 | - //! |
471 | - //! * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) |
472 | - //! * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) |
473 | - //! |
474 | - //! at your option. |
475 | - //! |
476 | - //! ## Copyright |
477 | - //! |
478 | - //! Copyright (C) 2020-2023, Stalwart Labs Ltd. |
479 | - //! |
480 | + #![doc = include_str!("../README.md")] |
481 | |
482 | use std::{ |
483 | cell::Cell, |