Commit
+120 -41 +/-6 browse
1 | diff --git a/src/common/base32.rs b/src/common/base32.rs |
2 | index f73cacb..a53b2c9 100644 |
3 | --- a/src/common/base32.rs |
4 | +++ b/src/common/base32.rs |
5 | @@ -8,9 +8,26 @@ |
6 | * except according to those terms. |
7 | */ |
8 | |
9 | + use std::slice::Iter; |
10 | + |
11 | use super::headers::Writer; |
12 | |
13 | pub(crate) static BASE32_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; |
14 | + pub static BASE32_INVERSE: [u8; 256] = [ |
15 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
16 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
17 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, 30, 31, 255, 255, |
18 | + 255, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
19 | + 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, |
20 | + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, |
21 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
22 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
23 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
24 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
25 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
26 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
27 | + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
28 | + ]; |
29 | |
30 | pub struct Base32Writer { |
31 | last_byte: u8, |
32 | @@ -82,10 +99,60 @@ impl Writer for Base32Writer { |
33 | } |
34 | } |
35 | |
36 | + #[derive(Debug)] |
37 | + pub struct Base32Reader<'x> { |
38 | + bytes: Iter<'x, u8>, |
39 | + last_byte: u8, |
40 | + pos: usize, |
41 | + } |
42 | + |
43 | + impl<'x> Base32Reader<'x> { |
44 | + pub fn new(bytes: &'x [u8]) -> Self { |
45 | + Base32Reader { |
46 | + bytes: bytes.iter(), |
47 | + pos: 0, |
48 | + last_byte: 0, |
49 | + } |
50 | + } |
51 | + |
52 | + #[inline(always)] |
53 | + fn map_byte(&mut self) -> Option<u8> { |
54 | + match self.bytes.next() { |
55 | + Some(&byte) => match BASE32_INVERSE[byte as usize] { |
56 | + byte if byte != u8::MAX => { |
57 | + self.last_byte = byte; |
58 | + Some(byte) |
59 | + } |
60 | + _ => None, |
61 | + }, |
62 | + _ => None, |
63 | + } |
64 | + } |
65 | + } |
66 | + |
67 | + impl Iterator for Base32Reader<'_> { |
68 | + type Item = u8; |
69 | + fn next(&mut self) -> Option<Self::Item> { |
70 | + let pos = self.pos % 5; |
71 | + let last_byte = self.last_byte; |
72 | + let byte = self.map_byte()?; |
73 | + self.pos += 1; |
74 | + |
75 | + match pos { |
76 | + 0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(), |
77 | + 1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(), |
78 | + 2 => ((last_byte << 4) | (byte >> 1)).into(), |
79 | + 3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(), |
80 | + 4 => ((last_byte << 5) | byte).into(), |
81 | + _ => None, |
82 | + } |
83 | + } |
84 | + } |
85 | + |
86 | #[cfg(test)] |
87 | mod tests { |
88 | use crate::common::{ |
89 | - base32::Base32Writer, |
90 | + base32::{Base32Reader, Base32Writer}, |
91 | crypto::{HashContext, HashImpl, Sha1}, |
92 | headers::Writer, |
93 | }; |
94 | @@ -96,11 +163,20 @@ mod tests { |
95 | ("one.example.net", "QSP4I4D24CRHOPDZ3O3ZIU2KSGS3X6Z6"), |
96 | ("two.example.net", "ZTZGRRV3F45A4U6HLDKBF3ZCOW4V2AJX"), |
97 | ] { |
98 | + // Encode |
99 | let mut writer = Base32Writer::with_capacity(10); |
100 | let mut hash = Sha1::hasher(); |
101 | hash.write(test.as_bytes()); |
102 | - writer.write(hash.complete().as_ref()); |
103 | + let hash = hash.complete(); |
104 | + writer.write(hash.as_ref()); |
105 | assert_eq!(writer.finalize(), expected_result); |
106 | + |
107 | + // Decode |
108 | + let mut original = Vec::new(); |
109 | + for byte in Base32Reader::new(expected_result.as_bytes()) { |
110 | + original.push(byte); |
111 | + } |
112 | + assert_eq!(original, hash.as_ref()); |
113 | } |
114 | } |
115 | } |
116 | diff --git a/src/lib.rs b/src/lib.rs |
117 | index 7b32ac8..9f54172 100644 |
118 | --- a/src/lib.rs |
119 | +++ b/src/lib.rs |
120 | @@ -282,6 +282,7 @@ pub mod mta_sts; |
121 | pub mod report; |
122 | pub mod spf; |
123 | |
124 | + pub use flate2; |
125 | pub use sha1; |
126 | pub use sha2; |
127 | pub use trust_dns_resolver; |
128 | diff --git a/src/report/arf/generate.rs b/src/report/arf/generate.rs |
129 | index 01b99ec..a3c126c 100644 |
130 | --- a/src/report/arf/generate.rs |
131 | +++ b/src/report/arf/generate.rs |
132 | @@ -11,7 +11,7 @@ |
133 | use std::{fmt::Write, io, time::SystemTime}; |
134 | |
135 | use mail_builder::{ |
136 | - headers::{content_type::ContentType, HeaderType}, |
137 | + headers::{address::Address, content_type::ContentType, HeaderType}, |
138 | mime::{make_boundary, BodyPart, MimePart}, |
139 | MessageBuilder, |
140 | }; |
141 | @@ -22,8 +22,7 @@ use crate::report::{AuthFailureType, DeliveryResult, Feedback, FeedbackType, Ide |
142 | impl<'x> Feedback<'x> { |
143 | pub fn write_rfc5322( |
144 | &self, |
145 | - from_name: &'x str, |
146 | - from_addr: &'x str, |
147 | + from: impl Into<Address<'x>>, |
148 | to: &'x str, |
149 | subject: &'x str, |
150 | writer: impl io::Write, |
151 | @@ -90,7 +89,7 @@ impl<'x> Feedback<'x> { |
152 | } |
153 | |
154 | MessageBuilder::new() |
155 | - .from((from_name, from_addr)) |
156 | + .from(from) |
157 | .header("To", HeaderType::Text(to.into())) |
158 | .header("Auto-Submitted", HeaderType::Text("auto-generated".into())) |
159 | .message_id(format!( |
160 | @@ -108,13 +107,12 @@ impl<'x> Feedback<'x> { |
161 | |
162 | pub fn to_rfc5322( |
163 | &self, |
164 | - from_name: &str, |
165 | - from_addr: &str, |
166 | - to: &str, |
167 | - subject: &str, |
168 | + from: impl Into<Address<'x>>, |
169 | + to: &'x str, |
170 | + subject: &'x str, |
171 | ) -> io::Result<String> { |
172 | let mut buf = Vec::new(); |
173 | - self.write_rfc5322(from_name, from_addr, to, subject, &mut buf)?; |
174 | + self.write_rfc5322(from, to, subject, &mut buf)?; |
175 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) |
176 | } |
177 | |
178 | @@ -287,8 +285,7 @@ mod test { |
179 | |
180 | let message = feedback |
181 | .to_rfc5322( |
182 | - "DMARC Reporter", |
183 | - "no-reply@example.org", |
184 | + ("DMARC Reporter", "no-reply@example.org"), |
185 | "ruf@otherdomain.com", |
186 | "DMARC Authentication Failure Report", |
187 | ) |
188 | diff --git a/src/report/dmarc/generate.rs b/src/report/dmarc/generate.rs |
189 | index 69d23dd..9824b12 100644 |
190 | --- a/src/report/dmarc/generate.rs |
191 | +++ b/src/report/dmarc/generate.rs |
192 | @@ -31,8 +31,7 @@ impl Report { |
193 | pub fn write_rfc5322<'x>( |
194 | &self, |
195 | submitter: &'x str, |
196 | - from_name: &'x str, |
197 | - from_addr: &'x str, |
198 | + from: impl Into<Address<'x>>, |
199 | to: impl Iterator<Item = &'x str>, |
200 | writer: impl io::Write, |
201 | ) -> io::Result<()> { |
202 | @@ -43,7 +42,7 @@ impl Report { |
203 | let compressed_bytes = e.finish()?; |
204 | |
205 | MessageBuilder::new() |
206 | - .from((from_name, from_addr)) |
207 | + .from(from) |
208 | .header( |
209 | "To", |
210 | HeaderType::Address(Address::List(to.map(|to| (*to).into()).collect())), |
211 | @@ -85,12 +84,11 @@ impl Report { |
212 | pub fn to_rfc5322<'x>( |
213 | &self, |
214 | submitter: &'x str, |
215 | - from_name: &'x str, |
216 | - from_addr: &'x str, |
217 | + from: impl Into<Address<'x>>, |
218 | to: impl Iterator<Item = &'x str>, |
219 | ) -> io::Result<String> { |
220 | let mut buf = Vec::new(); |
221 | - self.write_rfc5322(submitter, from_name, from_addr, to, &mut buf)?; |
222 | + self.write_rfc5322(submitter, from, to, &mut buf)?; |
223 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) |
224 | } |
225 | |
226 | @@ -524,9 +522,8 @@ mod test { |
227 | let message = report |
228 | .to_rfc5322( |
229 | "initech.net", |
230 | - "Initech Industries", |
231 | - "noreply-dmarc@initech.net", |
232 | - &["dmarc-reports@example.org"], |
233 | + ("Initech Industries", "noreply-dmarc@initech.net"), |
234 | + ["dmarc-reports@example.org"].iter().copied(), |
235 | ) |
236 | .unwrap(); |
237 | let parsed_report = Report::parse_rfc5322(message.as_bytes()).unwrap(); |
238 | diff --git a/src/report/tlsrpt/generate.rs b/src/report/tlsrpt/generate.rs |
239 | index 62a395a..f3be0af 100644 |
240 | --- a/src/report/tlsrpt/generate.rs |
241 | +++ b/src/report/tlsrpt/generate.rs |
242 | @@ -24,22 +24,32 @@ impl TlsReport { |
243 | &self, |
244 | report_domain: &'x str, |
245 | submitter: &'x str, |
246 | - from_name: &'x str, |
247 | - from_addr: &'x str, |
248 | - to: &'x [&str], |
249 | + from: impl Into<Address<'x>>, |
250 | + to: impl Iterator<Item = &'x str>, |
251 | writer: impl io::Write, |
252 | ) -> io::Result<()> { |
253 | // Compress JSON report |
254 | let json = self.to_json(); |
255 | let mut e = GzEncoder::new(Vec::with_capacity(json.len()), Compression::default()); |
256 | io::Write::write_all(&mut e, json.as_bytes())?; |
257 | - let compressed_bytes = e.finish()?; |
258 | + let bytes = e.finish()?; |
259 | + self.write_rfc5322_from_bytes(report_domain, submitter, from, to, &bytes, writer) |
260 | + } |
261 | |
262 | + pub fn write_rfc5322_from_bytes<'x>( |
263 | + &self, |
264 | + report_domain: &str, |
265 | + submitter: &str, |
266 | + from: impl Into<Address<'x>>, |
267 | + to: impl Iterator<Item = &'x str>, |
268 | + bytes: &[u8], |
269 | + writer: impl io::Write, |
270 | + ) -> io::Result<()> { |
271 | MessageBuilder::new() |
272 | - .from((from_name, from_addr)) |
273 | + .from(from) |
274 | .header( |
275 | "To", |
276 | - HeaderType::Address(Address::List(to.iter().map(|to| (*to).into()).collect())), |
277 | + HeaderType::Address(Address::List(to.map(|to| (*to).into()).collect())), |
278 | ) |
279 | .message_id(format!("<{}@{}>", make_boundary("."), submitter)) |
280 | .header("TLS-Report-Domain", HeaderType::Text(report_domain.into())) |
281 | @@ -69,7 +79,7 @@ impl TlsReport { |
282 | ), |
283 | MimePart::new( |
284 | ContentType::new("application/tlsrpt+gzip"), |
285 | - BodyPart::Binary(compressed_bytes.into()), |
286 | + BodyPart::Binary(bytes.into()), |
287 | ) |
288 | .attachment(format!( |
289 | "{}!{}!{}!{}.json.gz", |
290 | @@ -87,12 +97,11 @@ impl TlsReport { |
291 | &self, |
292 | report_domain: &'x str, |
293 | submitter: &'x str, |
294 | - from_name: &'x str, |
295 | - from_addr: &'x str, |
296 | - to: &'x [&str], |
297 | + from: impl Into<Address<'x>>, |
298 | + to: impl Iterator<Item = &'x str>, |
299 | ) -> io::Result<String> { |
300 | let mut buf = Vec::new(); |
301 | - self.write_rfc5322(report_domain, submitter, from_name, from_addr, to, &mut buf)?; |
302 | + self.write_rfc5322(report_domain, submitter, from, to, &mut buf)?; |
303 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) |
304 | } |
305 | |
306 | @@ -110,12 +119,12 @@ mod test { |
307 | #[test] |
308 | fn tlsrpt_generate() { |
309 | let report = TlsReport { |
310 | - organization_name: "Hello World, Inc.".to_string(), |
311 | + organization_name: "Hello World, Inc.".to_string().into(), |
312 | date_range: DateRange { |
313 | start_datetime: DateTime::from_timestamp(49823749), |
314 | end_datetime: DateTime::from_timestamp(49823899), |
315 | }, |
316 | - contact_info: "tls-report@hello-world.inc".to_string(), |
317 | + contact_info: "tls-report@hello-world.inc".to_string().into(), |
318 | report_id: "abc-123".to_string(), |
319 | policies: vec![], |
320 | }; |
321 | @@ -124,9 +133,8 @@ mod test { |
322 | .to_rfc5322( |
323 | "hello-world.inc", |
324 | "example.org", |
325 | - "mx.example.org", |
326 | "no-reply@example.org", |
327 | - &["tls-reports@hello-world.inc"], |
328 | + ["tls-reports@hello-world.inc"].iter().copied(), |
329 | ) |
330 | .unwrap(); |
331 | |
332 | diff --git a/src/report/tlsrpt/mod.rs b/src/report/tlsrpt/mod.rs |
333 | index 9038919..1cf4c1d 100644 |
334 | --- a/src/report/tlsrpt/mod.rs |
335 | +++ b/src/report/tlsrpt/mod.rs |
336 | @@ -20,14 +20,14 @@ pub mod parse; |
337 | pub struct TlsReport { |
338 | #[serde(rename = "organization-name")] |
339 | #[serde(default)] |
340 | - pub organization_name: String, |
341 | + pub organization_name: Option<String>, |
342 | |
343 | #[serde(rename = "date-range")] |
344 | pub date_range: DateRange, |
345 | |
346 | #[serde(rename = "contact-info")] |
347 | #[serde(default)] |
348 | - pub contact_info: String, |
349 | + pub contact_info: Option<String>, |
350 | |
351 | #[serde(rename = "report-id")] |
352 | #[serde(default)] |
353 | @@ -80,7 +80,7 @@ pub struct Summary { |
354 | pub total_failure: u32, |
355 | } |
356 | |
357 | - #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
358 | + #[derive(Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)] |
359 | pub struct FailureDetails { |
360 | #[serde(rename = "result-type")] |
361 | pub result_type: ResultType, |
362 | @@ -133,7 +133,7 @@ pub enum PolicyType { |
363 | Other, |
364 | } |
365 | |
366 | - #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
367 | + #[derive(Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)] |
368 | pub enum ResultType { |
369 | #[serde(rename = "starttls-not-supported")] |
370 | StartTlsNotSupported, |