Commit
+148 -22 +/-5 browse
1 | diff --git a/src/common/auth_results.rs b/src/common/auth_results.rs |
2 | index 8764db3..7e139c8 100644 |
3 | --- a/src/common/auth_results.rs |
4 | +++ b/src/common/auth_results.rs |
5 | @@ -61,17 +61,33 @@ impl<'x> AuthenticationResults<'x> { |
6 | self |
7 | } |
8 | |
9 | - pub fn with_spf_result( |
10 | + pub fn with_spf_ehlo_result( |
11 | mut self, |
12 | spf: &SpfOutput, |
13 | ip_addr: IpAddr, |
14 | - helo: &str, |
15 | - mail_from: &str, |
16 | + ehlo_domain: &str, |
17 | ) -> Self { |
18 | - let mail_from = if !mail_from.is_empty() { |
19 | - Cow::from(mail_from) |
20 | + self.auth_results.push_str(";\r\n\tspf="); |
21 | + spf.result.as_spf_result( |
22 | + &mut self.auth_results, |
23 | + self.hostname, |
24 | + &format!("postmaster@{}", ehlo_domain), |
25 | + ip_addr, |
26 | + ); |
27 | + write!(self.auth_results, " smtp.helo={}", ehlo_domain).ok(); |
28 | + self |
29 | + } |
30 | + |
31 | + pub fn with_spf_mailfrom_result( |
32 | + mut self, |
33 | + spf: &SpfOutput, |
34 | + ip_addr: IpAddr, |
35 | + from: &str, |
36 | + ) -> Self { |
37 | + let (mail_from, addr) = if !from.is_empty() { |
38 | + (Cow::from(from), from) |
39 | } else { |
40 | - format!("postmaster@{}", helo).into() |
41 | + (format!("postmaster@{}", from).into(), "<>") |
42 | }; |
43 | self.auth_results.push_str(";\r\n\tspf="); |
44 | spf.result.as_spf_result( |
45 | @@ -80,12 +96,7 @@ impl<'x> AuthenticationResults<'x> { |
46 | mail_from.as_ref(), |
47 | ip_addr, |
48 | ); |
49 | - write!( |
50 | - self.auth_results, |
51 | - " smtp.mailfrom={} smtp.helo={}", |
52 | - mail_from, helo |
53 | - ) |
54 | - .ok(); |
55 | + write!(self.auth_results, " smtp.mailfrom={}", addr).ok(); |
56 | self |
57 | } |
58 | |
59 | diff --git a/src/common/headers.rs b/src/common/headers.rs |
60 | index fe76b9b..56b9ce0 100644 |
61 | --- a/src/common/headers.rs |
62 | +++ b/src/common/headers.rs |
63 | @@ -33,6 +33,9 @@ pub(crate) struct HeaderParser<'x> { |
64 | message: &'x [u8], |
65 | iter: Peekable<Enumerate<Iter<'x, u8>>>, |
66 | start_pos: usize, |
67 | + pub num_received: usize, |
68 | + pub has_message_id: bool, |
69 | + pub has_date: bool, |
70 | } |
71 | |
72 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
73 | @@ -58,6 +61,9 @@ impl<'x> HeaderParser<'x> { |
74 | message, |
75 | iter: message.iter().enumerate().peekable(), |
76 | start_pos: 0, |
77 | + num_received: 0, |
78 | + has_message_id: false, |
79 | + has_date: false, |
80 | } |
81 | } |
82 | |
83 | @@ -223,6 +229,10 @@ impl<'x> Iterator for HeaderParser<'x> { |
84 | .unwrap_or_default(); |
85 | let header_value = self.message.get(colon_pos + 1..pos + 1).unwrap_or_default(); |
86 | let header_name = match hash { |
87 | + RECEIVED if token_start + 8 == token_end + 1 => { |
88 | + self.num_received += 1; |
89 | + AuthenticatedHeader::Other(header_name) |
90 | + } |
91 | FROM => AuthenticatedHeader::From(header_name), |
92 | AS => AuthenticatedHeader::As(header_name), |
93 | AAR if self |
94 | @@ -249,6 +259,20 @@ impl<'x> Iterator for HeaderParser<'x> { |
95 | { |
96 | AuthenticatedHeader::Ds(header_name) |
97 | } |
98 | + MSGID |
99 | + if self |
100 | + .message |
101 | + .get(token_start + 8..token_end + 1) |
102 | + .unwrap_or_default() |
103 | + .eq_ignore_ascii_case(b"id") => |
104 | + { |
105 | + self.has_message_id = true; |
106 | + AuthenticatedHeader::Other(header_name) |
107 | + } |
108 | + DATE => { |
109 | + self.has_date = true; |
110 | + AuthenticatedHeader::Other(header_name) |
111 | + } |
112 | _ => AuthenticatedHeader::Other(header_name), |
113 | }; |
114 | |
115 | @@ -321,6 +345,23 @@ const AS: u64 = (b'a' as u64) |
116 | | (b'e' as u64) << 40 |
117 | | (b'a' as u64) << 48 |
118 | | (b'l' as u64) << 56; |
119 | + const RECEIVED: u64 = (b'r' as u64) |
120 | + | (b'e' as u64) << 8 |
121 | + | (b'c' as u64) << 16 |
122 | + | (b'e' as u64) << 24 |
123 | + | (b'i' as u64) << 32 |
124 | + | (b'v' as u64) << 40 |
125 | + | (b'e' as u64) << 48 |
126 | + | (b'd' as u64) << 56; |
127 | + const DATE: u64 = (b'd' as u64) | (b'a' as u64) << 8 | (b't' as u64) << 16 | (b'e' as u64) << 24; |
128 | + const MSGID: u64 = (b'm' as u64) |
129 | + | (b'e' as u64) << 8 |
130 | + | (b's' as u64) << 16 |
131 | + | (b's' as u64) << 24 |
132 | + | (b'a' as u64) << 32 |
133 | + | (b'g' as u64) << 40 |
134 | + | (b'e' as u64) << 48 |
135 | + | (b'-' as u64) << 56; |
136 | |
137 | #[cfg(test)] |
138 | mod test { |
139 | @@ -408,12 +449,17 @@ mod test { |
140 | "From: jdoe@domain\n", |
141 | "F r o m : jane@domain.com\n", |
142 | "ARC-Authentication: i=1;\n", |
143 | + "Received: r1\n", |
144 | + "Received: r2\n", |
145 | + "Received: r3\n", |
146 | + "Received-From: test\n", |
147 | + "Date: date\n", |
148 | + "Message-Id: myid\n", |
149 | "\nhey", |
150 | ); |
151 | + let mut parser = HeaderParser::new(message.as_bytes()); |
152 | assert_eq!( |
153 | - HeaderParser::new(message.as_bytes()) |
154 | - .map(|(h, _)| { h }) |
155 | - .collect::<Vec<_>>(), |
156 | + (&mut parser).map(|(h, _)| { h }).collect::<Vec<_>>(), |
157 | vec![ |
158 | AuthenticatedHeader::Ams(b"ARC-Message-Signature"), |
159 | AuthenticatedHeader::Aar(b"ARC-Authentication-Results"), |
160 | @@ -422,7 +468,16 @@ mod test { |
161 | AuthenticatedHeader::From(b"From"), |
162 | AuthenticatedHeader::From(b"F r o m "), |
163 | AuthenticatedHeader::Other(b"ARC-Authentication"), |
164 | + AuthenticatedHeader::Other(b"Received"), |
165 | + AuthenticatedHeader::Other(b"Received"), |
166 | + AuthenticatedHeader::Other(b"Received"), |
167 | + AuthenticatedHeader::Other(b"Received-From"), |
168 | + AuthenticatedHeader::Other(b"Date"), |
169 | + AuthenticatedHeader::Other(b"Message-Id"), |
170 | ] |
171 | ); |
172 | + assert!(parser.has_date); |
173 | + assert!(parser.has_message_id); |
174 | + assert_eq!(parser.num_received, 3); |
175 | } |
176 | } |
177 | diff --git a/src/common/message.rs b/src/common/message.rs |
178 | index d55b158..0e35da0 100644 |
179 | --- a/src/common/message.rs |
180 | +++ b/src/common/message.rs |
181 | @@ -22,11 +22,15 @@ impl<'x> AuthenticatedMessage<'x> { |
182 | headers: Vec::new(), |
183 | from: Vec::new(), |
184 | body: b"", |
185 | + body_offset: 0, |
186 | body_hashes: Vec::new(), |
187 | dkim_headers: Vec::new(), |
188 | ams_headers: Vec::new(), |
189 | as_headers: Vec::new(), |
190 | aar_headers: Vec::new(), |
191 | + received_headers_count: 0, |
192 | + date_header_present: false, |
193 | + message_id_header_present: false, |
194 | }; |
195 | |
196 | let mut headers = HeaderParser::new(raw_message); |
197 | @@ -138,11 +142,18 @@ impl<'x> AuthenticatedMessage<'x> { |
198 | return None; |
199 | } |
200 | |
201 | + // Update header counts |
202 | + message.received_headers_count = headers.num_received; |
203 | + message.message_id_header_present = headers.has_message_id; |
204 | + message.date_header_present = headers.has_date; |
205 | + |
206 | // Obtain message body |
207 | - message.body = headers |
208 | - .body_offset() |
209 | - .and_then(|pos| raw_message.get(pos..)) |
210 | - .unwrap_or_default(); |
211 | + if let Some(offset) = headers.body_offset() { |
212 | + message.body_offset = offset; |
213 | + message.body = raw_message.get(offset..).unwrap_or_default(); |
214 | + } else { |
215 | + message.body_offset = raw_message.len(); |
216 | + } |
217 | |
218 | // Calculate body hashes |
219 | for (cb, ha, l, bh) in &mut message.body_hashes { |
220 | @@ -179,4 +190,24 @@ impl<'x> AuthenticatedMessage<'x> { |
221 | |
222 | message.into() |
223 | } |
224 | + |
225 | + pub fn received_headers_count(&self) -> usize { |
226 | + self.received_headers_count |
227 | + } |
228 | + |
229 | + pub fn has_message_id_header(&self) -> bool { |
230 | + self.message_id_header_present |
231 | + } |
232 | + |
233 | + pub fn has_date_header(&self) -> bool { |
234 | + self.date_header_present |
235 | + } |
236 | + |
237 | + pub fn body_offset(&self) -> usize { |
238 | + self.body_offset |
239 | + } |
240 | + |
241 | + pub fn from(&self) -> &[String] { |
242 | + &self.from |
243 | + } |
244 | } |
245 | diff --git a/src/lib.rs b/src/lib.rs |
246 | index 9407d2c..8e9d18b 100644 |
247 | --- a/src/lib.rs |
248 | +++ b/src/lib.rs |
249 | @@ -319,11 +319,15 @@ pub struct AuthenticatedMessage<'x> { |
250 | pub(crate) headers: Vec<(&'x [u8], &'x [u8])>, |
251 | pub(crate) from: Vec<String>, |
252 | pub(crate) body: &'x [u8], |
253 | + pub(crate) body_offset: usize, |
254 | pub(crate) body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>, |
255 | pub(crate) dkim_headers: Vec<Header<'x, crate::Result<dkim::Signature>>>, |
256 | pub(crate) ams_headers: Vec<Header<'x, crate::Result<arc::Signature>>>, |
257 | pub(crate) as_headers: Vec<Header<'x, crate::Result<arc::Seal>>>, |
258 | pub(crate) aar_headers: Vec<Header<'x, crate::Result<arc::Results>>>, |
259 | + pub(crate) received_headers_count: usize, |
260 | + pub(crate) date_header_present: bool, |
261 | + pub(crate) message_id_header_present: bool, |
262 | } |
263 | |
264 | #[derive(Debug, Clone, PartialEq, Eq)] |
265 | @@ -402,8 +406,8 @@ pub enum DmarcResult { |
266 | |
267 | #[derive(Debug, PartialEq, Eq, Clone)] |
268 | pub struct IprevOutput { |
269 | - result: IprevResult, |
270 | - ptr: Option<Arc<Vec<String>>>, |
271 | + pub result: IprevResult, |
272 | + pub ptr: Option<Arc<Vec<String>>>, |
273 | } |
274 | |
275 | #[derive(Debug, PartialEq, Eq, Clone)] |
276 | @@ -494,6 +498,20 @@ impl Display for Error { |
277 | } |
278 | } |
279 | |
280 | + impl Display for SpfResult { |
281 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
282 | + f.write_str(match self { |
283 | + SpfResult::Pass => "Pass", |
284 | + SpfResult::Fail => "Fail", |
285 | + SpfResult::SoftFail => "SoftFail", |
286 | + SpfResult::Neutral => "Neutral", |
287 | + SpfResult::TempError => "TempError", |
288 | + SpfResult::PermError => "PermError", |
289 | + SpfResult::None => "None", |
290 | + }) |
291 | + } |
292 | + } |
293 | + |
294 | impl From<io::Error> for Error { |
295 | fn from(err: io::Error) -> Self { |
296 | Error::Io(err.to_string()) |
297 | @@ -512,6 +530,17 @@ impl From<ed25519_dalek::ed25519::Error> for Error { |
298 | } |
299 | } |
300 | |
301 | + impl Default for SpfOutput { |
302 | + fn default() -> Self { |
303 | + Self { |
304 | + result: SpfResult::None, |
305 | + domain: Default::default(), |
306 | + report: Default::default(), |
307 | + explanation: Default::default(), |
308 | + } |
309 | + } |
310 | + } |
311 | + |
312 | thread_local!(static COUNTER: Cell<u64> = Cell::new(0)); |
313 | |
314 | /// Generates a random value between 0 and 100. |
315 | diff --git a/src/spf/verify.rs b/src/spf/verify.rs |
316 | index b489a7c..a5610bf 100644 |
317 | --- a/src/spf/verify.rs |
318 | +++ b/src/spf/verify.rs |
319 | @@ -77,7 +77,7 @@ impl Resolver { |
320 | } |
321 | |
322 | #[allow(clippy::while_let_on_iterator)] |
323 | - pub(crate) async fn check_host( |
324 | + pub async fn check_host( |
325 | &self, |
326 | ip: IpAddr, |
327 | domain: &str, |