Skip to main content

mail_auth/
message.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! Parsed view of an RFC 5322 message for authentication.
8//!
9//! [`AuthenticatedMessage`] splits a raw message into headers and body,
10//! parses the authentication headers it contains (`DKIM-Signature` from
11//! RFC 6376, `ARC-Seal`, `ARC-Message-Signature` and
12//! `ARC-Authentication-Results` from RFC 8617, `DKIM2-Signature` and
13//! `Message-Instance` from draft-ietf-dkim-dkim2-spec) and precomputes the
14//! body hashes the DKIM and ARC verifiers need. The verifiers on
15//! [`MessageAuthenticator`](crate::MessageAuthenticator) take it as input.
16
17#[cfg(feature = "arc")]
18use crate::arc;
19use crate::{
20    Error,
21    crypto::HashAlgorithm,
22    dkim::{self, Canonicalization},
23    dkim2,
24    headers::{AuthenticatedHeader, Header, HeaderParser},
25};
26use mail_parser::{AddressList, HeaderForm, HeaderName, Message};
27
28const EXPECTED_HEADER_COUNT: usize = 32;
29
30/// An RFC 5322 message prepared for DKIM, DKIM2, ARC and DMARC verification.
31///
32/// The message borrows the raw bytes it was parsed from. Parsing records the
33/// position of every header, the `From` addresses (used for DMARC, RFC 9989),
34/// the number of `Received` headers and the presence of `Date` and
35/// `Message-ID`. Authentication headers that fail to parse do not abort
36/// parsing: they are recorded in [`errors`](Self::errors) and flagged by
37/// [`has_dkim_errors`](Self::has_dkim_errors),
38/// [`has_dkim2_errors`](Self::has_dkim2_errors) and `has_arc_errors`.
39///
40/// # Example
41///
42/// ```rust,no_run
43/// use mail_auth::AuthenticatedMessage;
44///
45/// let raw = b"From: jdoe@example.org\r\nSubject: Hi\r\n\r\nHello\r\n";
46/// let message = AuthenticatedMessage::parse(raw).unwrap();
47/// assert_eq!(message.first_from_address(), "jdoe@example.org");
48/// assert_eq!(message.raw_body(), b"Hello\r\n");
49/// ```
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct AuthenticatedMessage<'x> {
52    pub(crate) headers: Vec<(&'x [u8], &'x [u8])>,
53    pub(crate) from: Vec<String>,
54    pub(crate) raw_message: &'x [u8],
55    pub(crate) body_offset: u32,
56    pub(crate) body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
57    pub(crate) dkim_headers: Vec<Header<'x, dkim::Signature>>,
58    pub(crate) dkim2_signatures: Vec<Header<'x, dkim2::Signature>>,
59    pub(crate) dkim2_instances: Vec<Header<'x, dkim2::MessageInstance>>,
60    #[cfg(feature = "arc")]
61    pub(crate) ams_headers: Vec<Header<'x, arc::Signature>>,
62    #[cfg(feature = "arc")]
63    pub(crate) as_headers: Vec<Header<'x, arc::Seal>>,
64    #[cfg(feature = "arc")]
65    pub(crate) aar_headers: Vec<Header<'x, arc::ArcAuthResults>>,
66    pub(crate) received_headers_count: usize,
67    pub(crate) date_header_present: bool,
68    pub(crate) message_id_header_present: bool,
69    pub(crate) errors: Vec<Header<'x, Error>>,
70    pub(crate) has_dkim_errors: bool,
71    #[cfg(feature = "arc")]
72    pub(crate) has_arc_errors: bool,
73    pub(crate) has_dkim2_errors: bool,
74}
75
76impl<'x> AsRef<AuthenticatedMessage<'x>> for AuthenticatedMessage<'x> {
77    fn as_ref(&self) -> &AuthenticatedMessage<'x> {
78        self
79    }
80}
81
82impl<'x> TryFrom<&'x [u8]> for AuthenticatedMessage<'x> {
83    type Error = Error;
84
85    fn try_from(value: &'x [u8]) -> Result<Self, Self::Error> {
86        AuthenticatedMessage::parse(value).ok_or(Error::Parse)
87    }
88}
89
90impl<'x> TryFrom<&'x Vec<u8>> for AuthenticatedMessage<'x> {
91    type Error = Error;
92
93    fn try_from(value: &'x Vec<u8>) -> Result<Self, Self::Error> {
94        AuthenticatedMessage::parse(value).ok_or(Error::Parse)
95    }
96}
97
98impl<'x> AuthenticatedMessage<'x> {
99    /// Parses a raw RFC 5322 message in strict mode.
100    ///
101    /// Equivalent to [`parse_with_opts(raw_message, None, true)`](Self::parse_with_opts).
102    /// Returns `None` when no header could be parsed.
103    pub fn parse(raw_message: &'x [u8]) -> Option<Self> {
104        Self::parse_with_opts(raw_message, None, true)
105    }
106
107    /// Builds an `AuthenticatedMessage` from a message already parsed by
108    /// [`mail_parser`], reusing its header offsets instead of parsing again.
109    ///
110    /// `raw_message` must be the exact bytes `parsed` was parsed from. Only the
111    /// headers of the root part are read. `strict` has the same meaning as in
112    /// [`parse_with_opts`](Self::parse_with_opts).
113    pub fn from_parsed<'y>(parsed: &'y Message<'x>, raw_message: &'x [u8], strict: bool) -> Self {
114        let root = parsed.root_part();
115        let headers = root.headers();
116        let mut message = AuthenticatedMessage {
117            raw_message,
118            body_offset: root.offset_body(),
119            headers: Vec::with_capacity(headers.len()),
120            ..Default::default()
121        };
122        let raw_range = |from: u32, to: u32| {
123            raw_message
124                .get(from as usize..to as usize)
125                .unwrap_or_default()
126        };
127
128        for header in headers {
129            let name = raw_range(
130                header.offset_field(),
131                header.offset_start().saturating_sub(1),
132            );
133            let value = raw_range(header.offset_start(), header.offset_end());
134
135            match header.name() {
136                HeaderName::From => {
137                    message.parse_from(header.value().as_address());
138                }
139                HeaderName::Date => {
140                    message.date_header_present = true;
141                }
142                HeaderName::Received => {
143                    message.received_headers_count += 1;
144                }
145                HeaderName::MessageId => {
146                    message.message_id_header_present = true;
147                }
148                HeaderName::DkimSignature => {
149                    message.parse_dkim(name, value, strict);
150                }
151                #[cfg(feature = "arc")]
152                HeaderName::ArcAuthenticationResults => {
153                    message.parse_aar(name, value);
154                }
155                #[cfg(feature = "arc")]
156                HeaderName::ArcSeal => {
157                    message.parse_as(name, value);
158                }
159                #[cfg(feature = "arc")]
160                HeaderName::ArcMessageSignature => {
161                    message.parse_ams(name, value, strict);
162                }
163                HeaderName::Dkim2Signature => {
164                    message.parse_dkim2_signature(name, value);
165                }
166                HeaderName::MessageInstance => {
167                    message.parse_dkim2_instance(name, value);
168                }
169                _ => (),
170            }
171
172            message.headers.push((name, value))
173        }
174
175        message.finalize()
176    }
177
178    /// Parses a raw RFC 5322 message.
179    ///
180    /// `prepend_headers`, when set, holds header lines that are parsed as if
181    /// they appeared before the headers of `raw_message` (for example,
182    /// headers added by the receiving MTA but not yet written to the message).
183    ///
184    /// When `strict` is `true`, `DKIM-Signature` and `ARC-Message-Signature`
185    /// headers that carry a body length tag (`l=`) are rejected and recorded
186    /// as errors ([`DkimError::BodyLengthTag`](crate::dkim::DkimError::BodyLengthTag)
187    /// for DKIM, `ArcError::BodyLengthTag` for ARC),
188    /// because a length limit lets an attacker append content to a signed
189    /// body (RFC 6376 Section 8.2). When `false`, such signatures are kept.
190    ///
191    /// Returns `None` when no header could be parsed.
192    pub fn parse_with_opts(
193        raw_message: &'x [u8],
194        prepend_headers: Option<&'x [u8]>,
195        strict: bool,
196    ) -> Option<Self> {
197        let mut message = AuthenticatedMessage {
198            raw_message,
199            headers: Vec::with_capacity(EXPECTED_HEADER_COUNT),
200            ..Default::default()
201        };
202
203        if let Some(headers) = prepend_headers {
204            message.parse_headers(headers, strict);
205        }
206
207        let body_offset = message.parse_headers(raw_message, strict);
208
209        if !message.headers.is_empty() {
210            if let Some(offset) = body_offset {
211                message.body_offset = offset as u32;
212            } else {
213                message.body_offset = raw_message.len() as u32;
214            }
215            Some(message.finalize())
216        } else {
217            None
218        }
219    }
220
221    fn parse_headers(&mut self, headers: &'x [u8], strict: bool) -> Option<usize> {
222        let mut headers = HeaderParser::new(headers);
223
224        for (header, value) in &mut headers {
225            let name = match header {
226                AuthenticatedHeader::Ds(name) => {
227                    self.parse_dkim(name, value, strict);
228                    name
229                }
230                AuthenticatedHeader::D2s(name) => {
231                    self.parse_dkim2_signature(name, value);
232                    name
233                }
234                AuthenticatedHeader::D2i(name) => {
235                    self.parse_dkim2_instance(name, value);
236                    name
237                }
238                #[cfg(feature = "arc")]
239                AuthenticatedHeader::Aar(name) => {
240                    self.parse_aar(name, value);
241                    name
242                }
243                #[cfg(feature = "arc")]
244                AuthenticatedHeader::Ams(name) => {
245                    self.parse_ams(name, value, strict);
246                    name
247                }
248                #[cfg(feature = "arc")]
249                AuthenticatedHeader::As(name) => {
250                    self.parse_as(name, value);
251                    name
252                }
253                AuthenticatedHeader::From(name) => {
254                    self.parse_from(HeaderForm::Addresses.parse(value).value().as_address());
255                    name
256                }
257                AuthenticatedHeader::Other(name) => name,
258            };
259
260            self.headers.push((name, value));
261        }
262
263        self.received_headers_count += headers.num_received;
264        self.message_id_header_present |= headers.has_message_id;
265        self.date_header_present |= headers.has_date;
266
267        headers.body_offset()
268    }
269
270    fn parse_dkim(&mut self, name: &'x [u8], value: &'x [u8], strict: bool) {
271        match dkim::Signature::parse(value) {
272            Ok(signature) if signature.l == 0 || !strict => {
273                let ha = HashAlgorithm::from(signature.a);
274                if !self
275                    .body_hashes
276                    .iter()
277                    .any(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
278                {
279                    self.body_hashes
280                        .push((signature.cb, ha, signature.l, Vec::new()));
281                }
282                self.dkim_headers.push(Header::new(name, value, signature));
283            }
284            Ok(_) => {
285                self.push_dkim_error(name, value, Error::Dkim(dkim::DkimError::BodyLengthTag));
286            }
287            Err(err) => self.push_dkim_error(name, value, err),
288        }
289    }
290
291    fn parse_dkim2_signature(&mut self, name: &'x [u8], value: &'x [u8]) {
292        match dkim2::Signature::parse(value) {
293            Ok(signature) => self
294                .dkim2_signatures
295                .push(Header::new(name, value, signature)),
296            Err(err) => self.push_dkim2_error(name, value, err),
297        }
298    }
299
300    fn parse_dkim2_instance(&mut self, name: &'x [u8], value: &'x [u8]) {
301        match dkim2::MessageInstance::parse(value) {
302            Ok(instance) => self
303                .dkim2_instances
304                .push(Header::new(name, value, instance)),
305            Err(err) => self.push_dkim2_error(name, value, err),
306        }
307    }
308
309    #[cfg(feature = "arc")]
310    fn parse_aar(&mut self, name: &'x [u8], value: &'x [u8]) {
311        match arc::ArcAuthResults::parse(value) {
312            Ok(results) => self.aar_headers.push(Header::new(name, value, results)),
313            Err(err) => self.push_arc_error(name, value, err),
314        }
315    }
316
317    #[cfg(feature = "arc")]
318    fn parse_ams(&mut self, name: &'x [u8], value: &'x [u8], strict: bool) {
319        match arc::Signature::parse(value) {
320            Ok(signature) if signature.l == 0 || !strict => {
321                let ha = HashAlgorithm::from(signature.a);
322                if !self
323                    .body_hashes
324                    .iter()
325                    .any(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
326                {
327                    self.body_hashes
328                        .push((signature.cb, ha, signature.l, Vec::new()));
329                }
330                self.ams_headers.push(Header::new(name, value, signature));
331            }
332            Ok(_) => {
333                self.push_arc_error(name, value, Error::Arc(arc::ArcError::BodyLengthTag));
334            }
335            Err(err) => self.push_arc_error(name, value, err),
336        }
337    }
338
339    #[cfg(feature = "arc")]
340    fn parse_as(&mut self, name: &'x [u8], value: &'x [u8]) {
341        match arc::Seal::parse(value) {
342            Ok(seal) => self.as_headers.push(Header::new(name, value, seal)),
343            Err(err) => self.push_arc_error(name, value, err),
344        }
345    }
346
347    fn push_dkim_error(&mut self, name: &'x [u8], value: &'x [u8], err: Error) {
348        self.has_dkim_errors = true;
349        self.errors.push(Header::new(name, value, err));
350    }
351
352    fn push_dkim2_error(&mut self, name: &'x [u8], value: &'x [u8], err: Error) {
353        self.has_dkim2_errors = true;
354        self.errors.push(Header::new(name, value, err));
355    }
356
357    #[cfg(feature = "arc")]
358    fn push_arc_error(&mut self, name: &'x [u8], value: &'x [u8], err: Error) {
359        self.has_arc_errors = true;
360        self.errors.push(Header::new(name, value, err));
361    }
362
363    fn parse_from(&mut self, addresses: Option<AddressList<'_>>) {
364        if let Some(addresses) = addresses {
365            self.from.extend(
366                addresses
367                    .mailboxes()
368                    .filter_map(|mailbox| mailbox.address().map(str::to_lowercase)),
369            );
370        }
371    }
372
373    fn finalize(mut self) -> Self {
374        let body = self
375            .raw_message
376            .get(self.body_offset as usize..)
377            .unwrap_or_default();
378
379        for (cb, ha, l, bh) in &mut self.body_hashes {
380            *bh = ha.hash(cb.canonical_body(body, *l)).as_ref().to_vec();
381        }
382
383        #[cfg(feature = "arc")]
384        if !self.as_headers.is_empty() && !self.has_arc_errors {
385            self.as_headers.sort_unstable_by_key(|h| h.header.i);
386            self.ams_headers.sort_unstable_by_key(|h| h.header.i);
387            self.aar_headers.sort_unstable_by_key(|h| h.header.i);
388        }
389
390        self.dkim2_signatures.sort_unstable_by_key(|h| h.header.i);
391        self.dkim2_instances.sort_unstable_by_key(|h| h.header.m);
392
393        self
394    }
395
396    /// Returns the number of `Received` headers in the message.
397    pub fn received_headers_count(&self) -> usize {
398        self.received_headers_count
399    }
400
401    /// Returns `true` if the message has a `Message-ID` header.
402    pub fn has_message_id_header(&self) -> bool {
403        self.message_id_header_present
404    }
405
406    /// Returns `true` if the message has a `Date` header.
407    pub fn has_date_header(&self) -> bool {
408        self.date_header_present
409    }
410
411    /// Returns the raw message bytes, headers and body.
412    pub fn raw_message(&self) -> &[u8] {
413        self.raw_message
414    }
415
416    /// Returns the raw header section of the message, including the blank
417    /// line that separates it from the body when one is present. Prepended
418    /// headers are not included.
419    pub fn raw_headers(&self) -> &[u8] {
420        self.raw_message
421            .get(..self.body_offset as usize)
422            .unwrap_or_default()
423    }
424
425    /// Returns every header as a `(name, value)` pair of raw bytes, in message
426    /// order. Prepended headers come first. The name excludes the colon; the
427    /// value keeps its folding and the trailing line ending.
428    pub fn headers(&self) -> &[(&'x [u8], &'x [u8])] {
429        &self.headers
430    }
431
432    /// Returns the raw message body. Empty when the message has no body.
433    pub fn raw_body(&self) -> &[u8] {
434        self.raw_message
435            .get(self.body_offset as usize..)
436            .unwrap_or_default()
437    }
438
439    /// Returns the byte offset of the body within
440    /// [`raw_message`](Self::raw_message).
441    pub fn body_offset(&self) -> usize {
442        self.body_offset as usize
443    }
444
445    /// Returns the lowercased addresses found in the `From` header, including
446    /// the members of address groups.
447    pub fn from_addresses(&self) -> &[String] {
448        &self.from
449    }
450
451    /// Returns the first `From` address, or an empty string if there is none.
452    /// This is the author address used for DMARC (RFC 9989).
453    pub fn first_from_address(&self) -> &str {
454        self.from.first().map_or("", |f| f.as_str())
455    }
456
457    /// Returns the `DKIM-Signature` headers (RFC 6376) that parsed
458    /// successfully, in message order.
459    pub fn dkim_signatures(&self) -> &[Header<'x, dkim::Signature>] {
460        &self.dkim_headers
461    }
462
463    /// Returns the `DKIM2-Signature` headers that parsed successfully, sorted
464    /// by signature sequence number (`i=` tag).
465    pub fn dkim2_signatures(&self) -> &[Header<'x, dkim2::Signature>] {
466        &self.dkim2_signatures
467    }
468
469    /// Returns the `Message-Instance` headers that parsed successfully,
470    /// sorted by revision number (`m=` tag).
471    pub fn dkim2_instances(&self) -> &[Header<'x, dkim2::MessageInstance>] {
472        &self.dkim2_instances
473    }
474
475    /// Returns the `ARC-Message-Signature` headers that parsed successfully
476    /// (feature `arc`). They are sorted by instance number (`i=` tag) unless
477    /// an ARC header failed to parse.
478    #[cfg(feature = "arc")]
479    pub fn arc_message_signatures(&self) -> &[Header<'x, arc::Signature>] {
480        &self.ams_headers
481    }
482
483    /// Returns the `ARC-Seal` headers that parsed successfully (feature
484    /// `arc`), sorted like [`arc_message_signatures`](Self::arc_message_signatures).
485    #[cfg(feature = "arc")]
486    pub fn arc_seals(&self) -> &[Header<'x, arc::Seal>] {
487        &self.as_headers
488    }
489
490    /// Returns the `ARC-Authentication-Results` headers that parsed
491    /// successfully (feature `arc`), sorted like
492    /// [`arc_message_signatures`](Self::arc_message_signatures).
493    #[cfg(feature = "arc")]
494    pub fn arc_authentication_results(&self) -> &[Header<'x, arc::ArcAuthResults>] {
495        &self.aar_headers
496    }
497
498    /// Returns the authentication headers that could not be parsed or were
499    /// rejected, each paired with the reason.
500    pub fn errors(&self) -> &[Header<'x, Error>] {
501        &self.errors
502    }
503
504    /// Returns `true` if a `DKIM-Signature` header failed to parse or was
505    /// rejected (including by
506    /// [`retain_dkim_signatures`](Self::retain_dkim_signatures)).
507    pub fn has_dkim_errors(&self) -> bool {
508        self.has_dkim_errors
509    }
510
511    /// Returns `true` if a `DKIM2-Signature` or `Message-Instance` header
512    /// failed to parse.
513    pub fn has_dkim2_errors(&self) -> bool {
514        self.has_dkim2_errors
515    }
516
517    /// Returns `true` if an ARC header failed to parse or was rejected. The
518    /// ARC verifier reports a broken chain in that case.
519    #[cfg(feature = "arc")]
520    pub fn has_arc_errors(&self) -> bool {
521        self.has_arc_errors
522    }
523
524    /// Keeps only the DKIM signatures accepted by `accept`.
525    ///
526    /// `accept` is called once for every parsed `DKIM-Signature`. Signatures
527    /// for which it returns `Err` are removed from
528    /// [`dkim_signatures`](Self::dkim_signatures); the error is added to
529    /// [`errors`](Self::errors) as [`Error::Dkim`] and
530    /// [`has_dkim_errors`](Self::has_dkim_errors) becomes `true`, so
531    /// `verify_dkim` reports each rejected signature as `neutral` with that
532    /// error. Use it to apply a local policy, such as rejecting `rsa-sha1`
533    /// signatures, before verification.
534    pub fn retain_dkim_signatures(
535        &mut self,
536        mut accept: impl FnMut(&dkim::Signature) -> Result<(), dkim::DkimError>,
537    ) {
538        let errors = &mut self.errors;
539        let mut rejected = false;
540        self.dkim_headers
541            .retain(|header| match accept(&header.header) {
542                Ok(()) => true,
543                Err(err) => {
544                    errors.push(Header::new(header.name, header.value, Error::Dkim(err)));
545                    rejected = true;
546                    false
547                }
548            });
549        self.has_dkim_errors |= rejected;
550    }
551}