Commit
Author: Mauro D [mauro@stalw.art]
Hash: 2a74f167f1512c4795eb527bfdb244c1b109ae9e
Timestamp: Fri, 13 Jan 2023 17:43:02 +0000 (1 year ago)

+131 -99 +/-13 browse
Reporting minor refactoring.
1diff --git a/resources/arf/002.json b/resources/arf/002.json
2index 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
15index 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
40index 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
114index 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
127index 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
156index 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
181index 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
198index 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
301index 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
354index 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
377index 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
556index 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
652index 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 }