Commit
+131 -99 +/-13 browse
1 | diff --git a/resources/arf/002.json b/resources/arf/002.json |
2 | index 6466697..49a33c7 100644 |
3 | --- a/resources/arf/002.json |
4 | +++ b/resources/arf/002.json |
5 | @@ -15,7 +15,7 @@ |
6 | "http://example.net/earn_money.html", |
7 | "mailto:user@example.com" |
8 | ], |
9 | - "reporting_mta": "dns; mail.example.com", |
10 | + "reporting_mta": "mail.example.com", |
11 | "source_ip": "192.0.2.1", |
12 | "user_agent": "SomeGenerator/1.0", |
13 | "version": 1, |
14 | diff --git a/src/common/base32.rs b/src/common/base32.rs |
15 | index 0a24a60..f73cacb 100644 |
16 | --- a/src/common/base32.rs |
17 | +++ b/src/common/base32.rs |
18 | @@ -12,13 +12,19 @@ use super::headers::Writer; |
19 | |
20 | pub(crate) static BASE32_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; |
21 | |
22 | - pub(crate) struct Base32Writer { |
23 | + pub struct Base32Writer { |
24 | last_byte: u8, |
25 | pos: usize, |
26 | result: String, |
27 | } |
28 | |
29 | impl Base32Writer { |
30 | + pub fn encode(bytes: &[u8]) -> String { |
31 | + let mut w = Base32Writer::with_capacity(bytes.len()); |
32 | + w.write(bytes); |
33 | + w.finalize() |
34 | + } |
35 | + |
36 | pub fn with_capacity(capacity: usize) -> Self { |
37 | Base32Writer { |
38 | result: String::with_capacity((capacity + 3) / 4 * 5), |
39 | diff --git a/src/dmarc/mod.rs b/src/dmarc/mod.rs |
40 | index b462429..00c307e 100644 |
41 | --- a/src/dmarc/mod.rs |
42 | +++ b/src/dmarc/mod.rs |
43 | @@ -10,12 +10,14 @@ |
44 | |
45 | use std::{fmt::Display, sync::Arc}; |
46 | |
47 | + use serde::{Deserialize, Serialize}; |
48 | + |
49 | use crate::{DmarcOutput, DmarcResult, Error, Version}; |
50 | |
51 | pub mod parse; |
52 | pub mod verify; |
53 | |
54 | - #[derive(Debug, Clone, PartialEq, Eq)] |
55 | + #[derive(Debug, Hash, Clone, PartialEq, Eq)] |
56 | pub struct Dmarc { |
57 | pub(crate) v: Version, |
58 | pub(crate) adkim: Alignment, |
59 | @@ -33,27 +35,27 @@ pub struct Dmarc { |
60 | pub(crate) t: bool, |
61 | } |
62 | |
63 | - #[derive(Debug, Clone, PartialEq, Eq)] |
64 | + #[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] |
65 | #[allow(clippy::upper_case_acronyms)] |
66 | pub struct URI { |
67 | - uri: String, |
68 | - max_size: usize, |
69 | + pub uri: String, |
70 | + pub max_size: usize, |
71 | } |
72 | |
73 | - #[derive(Debug, Clone, PartialEq, Eq)] |
74 | + #[derive(Debug, Hash, Clone, PartialEq, Eq)] |
75 | pub(crate) enum Alignment { |
76 | Relaxed, |
77 | Strict, |
78 | } |
79 | |
80 | - #[derive(Debug, Clone, PartialEq, Eq)] |
81 | + #[derive(Debug, Hash, Clone, PartialEq, Eq)] |
82 | pub(crate) enum Psd { |
83 | Yes, |
84 | No, |
85 | Default, |
86 | } |
87 | |
88 | - #[derive(Debug, Clone, PartialEq, Eq)] |
89 | + #[derive(Debug, Hash, Clone, PartialEq, Eq)] |
90 | pub enum Report { |
91 | All, |
92 | Any, |
93 | @@ -62,7 +64,7 @@ pub enum Report { |
94 | DkimSpf, |
95 | } |
96 | |
97 | - #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
98 | + #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] |
99 | pub enum Policy { |
100 | None, |
101 | Quarantine, |
102 | @@ -147,6 +149,10 @@ impl DmarcOutput { |
103 | &self.domain |
104 | } |
105 | |
106 | + pub fn into_domain(self) -> String { |
107 | + self.domain |
108 | + } |
109 | + |
110 | pub fn policy(&self) -> Policy { |
111 | self.policy |
112 | } |
113 | diff --git a/src/lib.rs b/src/lib.rs |
114 | index f2714e7..7b32ac8 100644 |
115 | --- a/src/lib.rs |
116 | +++ b/src/lib.rs |
117 | @@ -419,7 +419,7 @@ pub enum IprevResult { |
118 | None, |
119 | } |
120 | |
121 | - #[derive(Debug, PartialEq, Eq, Clone)] |
122 | + #[derive(Debug, Hash, PartialEq, Eq, Clone)] |
123 | pub(crate) enum Version { |
124 | V1, |
125 | } |
126 | diff --git a/src/mta_sts/mod.rs b/src/mta_sts/mod.rs |
127 | index 31e9728..632fba1 100644 |
128 | --- a/src/mta_sts/mod.rs |
129 | +++ b/src/mta_sts/mod.rs |
130 | @@ -8,6 +8,8 @@ |
131 | * except according to those terms. |
132 | */ |
133 | |
134 | + use serde::{Deserialize, Serialize}; |
135 | + |
136 | pub mod parse; |
137 | |
138 | #[derive(Debug, PartialEq, Eq)] |
139 | @@ -15,12 +17,12 @@ pub struct MtaSts { |
140 | pub id: String, |
141 | } |
142 | |
143 | - #[derive(Debug, PartialEq, Eq)] |
144 | + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
145 | pub struct TlsRpt { |
146 | - rua: Vec<ReportUri>, |
147 | + pub rua: Vec<ReportUri>, |
148 | } |
149 | |
150 | - #[derive(Debug, PartialEq, Eq)] |
151 | + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
152 | pub enum ReportUri { |
153 | Mail(String), |
154 | Http(String), |
155 | diff --git a/src/report/arf/generate.rs b/src/report/arf/generate.rs |
156 | index 972bb4a..01b99ec 100644 |
157 | --- a/src/report/arf/generate.rs |
158 | +++ b/src/report/arf/generate.rs |
159 | @@ -12,7 +12,7 @@ use std::{fmt::Write, io, time::SystemTime}; |
160 | |
161 | use mail_builder::{ |
162 | headers::{content_type::ContentType, HeaderType}, |
163 | - mime::{BodyPart, MimePart}, |
164 | + mime::{make_boundary, BodyPart, MimePart}, |
165 | MessageBuilder, |
166 | }; |
167 | use mail_parser::DateTime; |
168 | @@ -93,6 +93,11 @@ impl<'x> Feedback<'x> { |
169 | .from((from_name, from_addr)) |
170 | .header("To", HeaderType::Text(to.into())) |
171 | .header("Auto-Submitted", HeaderType::Text("auto-generated".into())) |
172 | + .message_id(format!( |
173 | + "<{}@{}>", |
174 | + make_boundary("."), |
175 | + self.reporting_mta().unwrap_or("localhost") |
176 | + )) |
177 | .subject(subject) |
178 | .body(MimePart::new( |
179 | ContentType::new("multipart/report").attribute("report-type", "feedback-report"), |
180 | diff --git a/src/report/arf/parse.rs b/src/report/arf/parse.rs |
181 | index 81c04fa..b5ecf0f 100644 |
182 | --- a/src/report/arf/parse.rs |
183 | +++ b/src/report/arf/parse.rs |
184 | @@ -209,7 +209,11 @@ impl<'x> Feedback<'x> { |
185 | } else if key.eq_ignore_ascii_case(b"Reported-URI") { |
186 | f.reported_uri.push(txt_value.into()); |
187 | } else if key.eq_ignore_ascii_case(b"Reporting-MTA") { |
188 | - f.reporting_mta = Some(txt_value.into()); |
189 | + f.reporting_mta = Some(if let Some(mta) = txt_value.strip_prefix("dns;") { |
190 | + mta.trim().into() |
191 | + } else { |
192 | + txt_value.into() |
193 | + }); |
194 | } else if key.eq_ignore_ascii_case(b"Received-Date") { |
195 | if let HeaderValue::DateTime(dt) = MessageStream::new(value).parse_date() { |
196 | f.arrival_date = dt.to_timestamp().into(); |
197 | diff --git a/src/report/dmarc/generate.rs b/src/report/dmarc/generate.rs |
198 | index 0c7389e..69d23dd 100644 |
199 | --- a/src/report/dmarc/generate.rs |
200 | +++ b/src/report/dmarc/generate.rs |
201 | @@ -9,7 +9,11 @@ |
202 | */ |
203 | |
204 | use flate2::{write::GzEncoder, Compression}; |
205 | - use mail_builder::{headers::HeaderType, MessageBuilder}; |
206 | + use mail_builder::{ |
207 | + headers::{address::Address, HeaderType}, |
208 | + mime::make_boundary, |
209 | + MessageBuilder, |
210 | + }; |
211 | |
212 | use crate::report::{ |
213 | ActionDisposition, Alignment, AuthResult, DKIMAuthResult, DateRange, Disposition, DkimResult, |
214 | @@ -26,10 +30,10 @@ use std::{ |
215 | impl Report { |
216 | pub fn write_rfc5322<'x>( |
217 | &self, |
218 | - receiver_domain: &'x str, |
219 | submitter: &'x str, |
220 | - from: &'x str, |
221 | - to: &'x str, |
222 | + from_name: &'x str, |
223 | + from_addr: &'x str, |
224 | + to: impl Iterator<Item = &'x str>, |
225 | writer: impl io::Write, |
226 | ) -> io::Result<()> { |
227 | // Compress XML report |
228 | @@ -39,9 +43,13 @@ impl Report { |
229 | let compressed_bytes = e.finish()?; |
230 | |
231 | MessageBuilder::new() |
232 | - .header("From", HeaderType::Text(from.into())) |
233 | - .header("To", HeaderType::Text(to.into())) |
234 | + .from((from_name, from_addr)) |
235 | + .header( |
236 | + "To", |
237 | + HeaderType::Address(Address::List(to.map(|to| (*to).into()).collect())), |
238 | + ) |
239 | .header("Auto-Submitted", HeaderType::Text("auto-generated".into())) |
240 | + .message_id(format!("<{}@{}>", make_boundary("."), submitter)) |
241 | .subject(format!( |
242 | "Report Domain: {} Submitter: {} Report-ID: <{}>", |
243 | self.domain(), |
244 | @@ -55,7 +63,7 @@ impl Report { |
245 | "Submitter: {}\r\n", |
246 | "Report-ID: {}\r\n", |
247 | ), |
248 | - receiver_domain, |
249 | + submitter, |
250 | self.domain(), |
251 | submitter, |
252 | self.report_id() |
253 | @@ -64,7 +72,7 @@ impl Report { |
254 | "application/gzip", |
255 | format!( |
256 | "{}!{}!{}!{}.xml.gz", |
257 | - receiver_domain, |
258 | + submitter, |
259 | self.domain(), |
260 | self.date_range_begin(), |
261 | self.date_range_end() |
262 | @@ -76,13 +84,13 @@ impl Report { |
263 | |
264 | pub fn to_rfc5322<'x>( |
265 | &self, |
266 | - receiver_domain: &'x str, |
267 | submitter: &'x str, |
268 | - from: &'x str, |
269 | - to: &'x str, |
270 | + from_name: &'x str, |
271 | + from_addr: &'x str, |
272 | + to: impl Iterator<Item = &'x str>, |
273 | ) -> io::Result<String> { |
274 | let mut buf = Vec::new(); |
275 | - self.write_rfc5322(receiver_domain, submitter, from, to, &mut buf)?; |
276 | + self.write_rfc5322(submitter, from_name, from_addr, to, &mut buf)?; |
277 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) |
278 | } |
279 | |
280 | @@ -178,7 +186,9 @@ impl Record { |
281 | impl Row { |
282 | pub(crate) fn to_xml(&self, xml: &mut String) { |
283 | writeln!(xml, "\t\t<row>").ok(); |
284 | - writeln!(xml, "\t\t\t<source_ip>{}</source_ip>", self.source_ip).ok(); |
285 | + if let Some(source_ip) = &self.source_ip { |
286 | + writeln!(xml, "\t\t\t<source_ip>{}</source_ip>", source_ip).ok(); |
287 | + } |
288 | writeln!(xml, "\t\t\t<count>{}</count>", self.count).ok(); |
289 | self.policy_evaluated.to_xml(xml); |
290 | writeln!(xml, "\t\t</row>").ok(); |
291 | @@ -516,7 +526,7 @@ mod test { |
292 | "initech.net", |
293 | "Initech Industries", |
294 | "noreply-dmarc@initech.net", |
295 | - "dmarc-reports@example.org", |
296 | + &["dmarc-reports@example.org"], |
297 | ) |
298 | .unwrap(); |
299 | let parsed_report = Report::parse_rfc5322(message.as_bytes()).unwrap(); |
300 | diff --git a/src/report/dmarc/mod.rs b/src/report/dmarc/mod.rs |
301 | index 7049982..566fd80 100644 |
302 | --- a/src/report/dmarc/mod.rs |
303 | +++ b/src/report/dmarc/mod.rs |
304 | @@ -15,6 +15,7 @@ use std::fmt::Write; |
305 | use std::net::IpAddr; |
306 | |
307 | use crate::{ |
308 | + dmarc::Dmarc, |
309 | report::{ |
310 | ActionDisposition, Alignment, DKIMAuthResult, Disposition, DkimResult, DmarcResult, |
311 | PolicyOverride, PolicyOverrideReason, Record, Report, SPFAuthResult, SPFDomainScope, |
312 | @@ -183,6 +184,10 @@ impl Report { |
313 | self |
314 | } |
315 | |
316 | + pub fn add_record(&mut self, record: Record) { |
317 | + self.record.push(record); |
318 | + } |
319 | + |
320 | pub fn with_policy_published(mut self, policy_published: PolicyPublished) -> Self { |
321 | self.policy_published = policy_published; |
322 | self |
323 | @@ -273,12 +278,12 @@ impl Record { |
324 | self |
325 | } |
326 | |
327 | - pub fn source_ip(&self) -> IpAddr { |
328 | + pub fn source_ip(&self) -> Option<IpAddr> { |
329 | self.row.source_ip |
330 | } |
331 | |
332 | pub fn with_source_ip(mut self, source_ip: IpAddr) -> Self { |
333 | - self.row.source_ip = source_ip; |
334 | + self.row.source_ip = source_ip.into(); |
335 | self |
336 | } |
337 | |
338 | @@ -373,11 +378,10 @@ impl Record { |
339 | } |
340 | } |
341 | |
342 | - impl From<DmarcOutput> for PolicyPublished { |
343 | - fn from(value: DmarcOutput) -> Self { |
344 | - let dmarc = value.record.unwrap(); |
345 | + impl PolicyPublished { |
346 | + pub fn from_record(domain: impl Into<String>, dmarc: &Dmarc) -> Self { |
347 | PolicyPublished { |
348 | - domain: value.domain, |
349 | + domain: domain.into(), |
350 | adkim: (&dmarc.adkim).into(), |
351 | aspf: (&dmarc.aspf).into(), |
352 | p: (&dmarc.p).into(), |
353 | diff --git a/src/report/dmarc/parse.rs b/src/report/dmarc/parse.rs |
354 | index 1aa5f36..274b5a9 100644 |
355 | --- a/src/report/dmarc/parse.rs |
356 | +++ b/src/report/dmarc/parse.rs |
357 | @@ -9,6 +9,7 @@ |
358 | */ |
359 | |
360 | use std::io::{BufRead, Cursor, Read}; |
361 | + use std::net::IpAddr; |
362 | use std::str::FromStr; |
363 | |
364 | use flate2::read::GzDecoder; |
365 | @@ -375,8 +376,8 @@ impl Row { |
366 | while let Some(tag) = reader.next_tag(buf)? { |
367 | match tag.name().as_ref() { |
368 | b"source_ip" => { |
369 | - if let Some(ip) = reader.next_value(buf)? { |
370 | - r.source_ip = ip; |
371 | + if let Some(ip) = reader.next_value::<IpAddr>(buf)? { |
372 | + r.source_ip = ip.into(); |
373 | } |
374 | } |
375 | b"count" => { |
376 | diff --git a/src/report/mod.rs b/src/report/mod.rs |
377 | index 6a94f4a..1b955a4 100644 |
378 | --- a/src/report/mod.rs |
379 | +++ b/src/report/mod.rs |
380 | @@ -12,10 +12,7 @@ pub mod arf; |
381 | pub mod dmarc; |
382 | pub mod tlsrpt; |
383 | |
384 | - use std::{ |
385 | - borrow::Cow, |
386 | - net::{IpAddr, Ipv4Addr}, |
387 | - }; |
388 | + use std::{borrow::Cow, net::IpAddr}; |
389 | |
390 | use serde::{Deserialize, Serialize}; |
391 | |
392 | @@ -50,7 +47,7 @@ pub enum Disposition { |
393 | Unspecified, |
394 | } |
395 | |
396 | - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
397 | + #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
398 | pub enum ActionDisposition { |
399 | None, |
400 | Pass, |
401 | @@ -61,26 +58,26 @@ pub enum ActionDisposition { |
402 | |
403 | #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] |
404 | pub struct PolicyPublished { |
405 | - domain: String, |
406 | - version_published: Option<f32>, |
407 | - adkim: Alignment, |
408 | - aspf: Alignment, |
409 | - p: Disposition, |
410 | - sp: Disposition, |
411 | - testing: bool, |
412 | - fo: Option<String>, |
413 | + pub domain: String, |
414 | + pub version_published: Option<f32>, |
415 | + pub adkim: Alignment, |
416 | + pub aspf: Alignment, |
417 | + pub p: Disposition, |
418 | + pub sp: Disposition, |
419 | + pub testing: bool, |
420 | + pub fo: Option<String>, |
421 | } |
422 | |
423 | impl Eq for PolicyPublished {} |
424 | |
425 | - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
426 | + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] |
427 | pub enum DmarcResult { |
428 | Pass, |
429 | Fail, |
430 | Unspecified, |
431 | } |
432 | |
433 | - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
434 | + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] |
435 | pub enum PolicyOverride { |
436 | Forwarded, |
437 | SampledOut, |
438 | @@ -90,13 +87,13 @@ pub enum PolicyOverride { |
439 | Other, |
440 | } |
441 | |
442 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
443 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
444 | pub struct PolicyOverrideReason { |
445 | type_: PolicyOverride, |
446 | comment: Option<String>, |
447 | } |
448 | |
449 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
450 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
451 | pub struct PolicyEvaluated { |
452 | disposition: ActionDisposition, |
453 | dkim: DmarcResult, |
454 | @@ -104,27 +101,27 @@ pub struct PolicyEvaluated { |
455 | reason: Vec<PolicyOverrideReason>, |
456 | } |
457 | |
458 | - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
459 | + #[derive(Debug, Clone, Hash, Default, PartialEq, Eq, Serialize, Deserialize)] |
460 | pub struct Row { |
461 | - source_ip: IpAddr, |
462 | + source_ip: Option<IpAddr>, |
463 | count: u32, |
464 | policy_evaluated: PolicyEvaluated, |
465 | } |
466 | |
467 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
468 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
469 | pub struct Extension { |
470 | name: String, |
471 | definition: String, |
472 | } |
473 | |
474 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
475 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
476 | pub struct Identifier { |
477 | envelope_to: Option<String>, |
478 | envelope_from: String, |
479 | header_from: String, |
480 | } |
481 | |
482 | - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
483 | + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] |
484 | pub enum DkimResult { |
485 | None, |
486 | Pass, |
487 | @@ -135,7 +132,7 @@ pub enum DkimResult { |
488 | PermError, |
489 | } |
490 | |
491 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
492 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
493 | pub struct DKIMAuthResult { |
494 | domain: String, |
495 | selector: String, |
496 | @@ -143,14 +140,14 @@ pub struct DKIMAuthResult { |
497 | human_result: Option<String>, |
498 | } |
499 | |
500 | - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
501 | + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] |
502 | pub enum SPFDomainScope { |
503 | Helo, |
504 | MailFrom, |
505 | Unspecified, |
506 | } |
507 | |
508 | - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
509 | + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] |
510 | pub enum SpfResult { |
511 | None, |
512 | Neutral, |
513 | @@ -161,7 +158,7 @@ pub enum SpfResult { |
514 | PermError, |
515 | } |
516 | |
517 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
518 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
519 | pub struct SPFAuthResult { |
520 | domain: String, |
521 | scope: SPFDomainScope, |
522 | @@ -169,13 +166,13 @@ pub struct SPFAuthResult { |
523 | human_result: Option<String>, |
524 | } |
525 | |
526 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
527 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
528 | pub struct AuthResult { |
529 | dkim: Vec<DKIMAuthResult>, |
530 | spf: Vec<SPFAuthResult>, |
531 | } |
532 | |
533 | - #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
534 | + #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] |
535 | pub struct Record { |
536 | row: Row, |
537 | identifiers: Identifier, |
538 | @@ -194,16 +191,6 @@ pub struct Report { |
539 | |
540 | impl Eq for Report {} |
541 | |
542 | - impl Default for Row { |
543 | - fn default() -> Self { |
544 | - Self { |
545 | - source_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), |
546 | - count: 0, |
547 | - policy_evaluated: PolicyEvaluated::default(), |
548 | - } |
549 | - } |
550 | - } |
551 | - |
552 | impl Default for Alignment { |
553 | fn default() -> Self { |
554 | Alignment::Unspecified |
555 | diff --git a/src/report/tlsrpt/generate.rs b/src/report/tlsrpt/generate.rs |
556 | index bfe1d78..62a395a 100644 |
557 | --- a/src/report/tlsrpt/generate.rs |
558 | +++ b/src/report/tlsrpt/generate.rs |
559 | @@ -12,8 +12,8 @@ use std::io; |
560 | |
561 | use flate2::{write::GzEncoder, Compression}; |
562 | use mail_builder::{ |
563 | - headers::{content_type::ContentType, HeaderType}, |
564 | - mime::{BodyPart, MimePart}, |
565 | + headers::{address::Address, content_type::ContentType, HeaderType}, |
566 | + mime::{make_boundary, BodyPart, MimePart}, |
567 | MessageBuilder, |
568 | }; |
569 | |
570 | @@ -23,10 +23,10 @@ impl TlsReport { |
571 | pub fn write_rfc5322<'x>( |
572 | &self, |
573 | report_domain: &'x str, |
574 | - receiver_domain: &'x str, |
575 | submitter: &'x str, |
576 | - from: &'x str, |
577 | - to: &'x str, |
578 | + from_name: &'x str, |
579 | + from_addr: &'x str, |
580 | + to: &'x [&str], |
581 | writer: impl io::Write, |
582 | ) -> io::Result<()> { |
583 | // Compress JSON report |
584 | @@ -36,8 +36,12 @@ impl TlsReport { |
585 | let compressed_bytes = e.finish()?; |
586 | |
587 | MessageBuilder::new() |
588 | - .header("From", HeaderType::Text(from.into())) |
589 | - .header("To", HeaderType::Text(to.into())) |
590 | + .from((from_name, from_addr)) |
591 | + .header( |
592 | + "To", |
593 | + HeaderType::Address(Address::List(to.iter().map(|to| (*to).into()).collect())), |
594 | + ) |
595 | + .message_id(format!("<{}@{}>", make_boundary("."), submitter)) |
596 | .header("TLS-Report-Domain", HeaderType::Text(report_domain.into())) |
597 | .header("TLS-Report-Submitter", HeaderType::Text(submitter.into())) |
598 | .header("Auto-Submitted", HeaderType::Text("auto-generated".into())) |
599 | @@ -58,7 +62,7 @@ impl TlsReport { |
600 | "Submitter: {}\r\n", |
601 | "Report-ID: {}\r\n", |
602 | ), |
603 | - receiver_domain, report_domain, submitter, self.report_id |
604 | + submitter, report_domain, submitter, self.report_id |
605 | ) |
606 | .into(), |
607 | ), |
608 | @@ -69,7 +73,7 @@ impl TlsReport { |
609 | ) |
610 | .attachment(format!( |
611 | "{}!{}!{}!{}.json.gz", |
612 | - receiver_domain, |
613 | + submitter, |
614 | report_domain, |
615 | self.date_range.start_datetime.to_timestamp(), |
616 | self.date_range.end_datetime.to_timestamp() |
617 | @@ -82,20 +86,13 @@ impl TlsReport { |
618 | pub fn to_rfc5322<'x>( |
619 | &self, |
620 | report_domain: &'x str, |
621 | - receiver_domain: &'x str, |
622 | submitter: &'x str, |
623 | - from: &'x str, |
624 | - to: &'x str, |
625 | + from_name: &'x str, |
626 | + from_addr: &'x str, |
627 | + to: &'x [&str], |
628 | ) -> io::Result<String> { |
629 | let mut buf = Vec::new(); |
630 | - self.write_rfc5322( |
631 | - report_domain, |
632 | - receiver_domain, |
633 | - submitter, |
634 | - from, |
635 | - to, |
636 | - &mut buf, |
637 | - )?; |
638 | + self.write_rfc5322(report_domain, submitter, from_name, from_addr, to, &mut buf)?; |
639 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) |
640 | } |
641 | |
642 | @@ -129,7 +126,7 @@ mod test { |
643 | "example.org", |
644 | "mx.example.org", |
645 | "no-reply@example.org", |
646 | - "tls-reports@hello-world.inc", |
647 | + &["tls-reports@hello-world.inc"], |
648 | ) |
649 | .unwrap(); |
650 | |
651 | diff --git a/src/report/tlsrpt/mod.rs b/src/report/tlsrpt/mod.rs |
652 | index c95fa54..9038919 100644 |
653 | --- a/src/report/tlsrpt/mod.rs |
654 | +++ b/src/report/tlsrpt/mod.rs |
655 | @@ -198,8 +198,18 @@ impl FailureDetails { |
656 | } |
657 | } |
658 | |
659 | - pub fn with_failure_reason_code(mut self, code: impl Into<String>) -> Self { |
660 | - self.failure_reason_code = Some(code.into()); |
661 | + pub fn with_failure_reason_code(mut self, value: impl Into<String>) -> Self { |
662 | + self.failure_reason_code = Some(value.into()); |
663 | + self |
664 | + } |
665 | + |
666 | + pub fn with_receiving_mx_hostname(mut self, value: impl Into<String>) -> Self { |
667 | + self.receiving_mx_hostname = Some(value.into()); |
668 | + self |
669 | + } |
670 | + |
671 | + pub fn with_receiving_ip(mut self, value: IpAddr) -> Self { |
672 | + self.receiving_ip = Some(value); |
673 | self |
674 | } |
675 | } |