Skip to main content

parse_net/mail/
mod.rs

1//! The email lexical layer (RFC 5322, RFC 2045, RFC 2047, RFC 2231): header
2//! lines and folding, encoded words, MIME parameters with continuations,
3//! addresses, and message identifiers. The grammar smtp-net needs to write
4//! mail and gate-net needs to validate login addresses (D-008); not a mail
5//! parser. Charsets decoded: UTF-8, US-ASCII, ISO-8859-1.
6//!
7//! # Examples
8//!
9//! ```
10//! use parse_net::{mail, SliceSink, Limits};
11//!
12//! let mut buf = [0u8; 256];
13//! let mut out = SliceSink::new(&mut buf);
14//! mail::encode_words("Caf\u{e9} au lait", 64, &mut out).expect("room");
15//! assert_eq!(out.written(), b"=?UTF-8?Q?Caf=C3=A9_au_lait?=");
16//!
17//! let a = mail::parse_addr_spec(b"\"john doe\"@example.com")?;
18//! assert!(a.is_ascii());
19//! assert!(mail::parse_addr_spec(b"john..doe@example.com").is_err());
20//!
21//! let (p, _) = mail::parse_mime_params(b"; filename*0*=UTF-8''caf%C3%A9; filename*1=\".txt\"", &Limits::DEFAULT)?;
22//! let mut name = [0u8; 32];
23//! assert_eq!(p.decode_into("filename", &mut name)?, Some("caf\u{e9}.txt"));
24//! # Ok::<(), parse_net::ParseError>(())
25//! ```
26
27mod addr;
28mod params;
29mod words;
30
31pub use addr::{
32    AddrSpec, Domain, LocalPart, Mailbox, MsgId, parse_addr_spec, parse_mailbox, parse_msg_id,
33    write_msg_id,
34};
35pub use params::{
36    Continuation, MimeParamIter, MimeParams, MimeValue, parse_mime_params, write_mime_param,
37};
38pub use words::{decode_words, encode_words};
39
40pub use crate::MailDate;
41
42use crate::{
43    error::{ErrorKind, Result, WriteError, err},
44    scan::control_kind,
45    write::{CountSink, Sink, reserve},
46};
47
48/// The longest physical line, CRLF excluded (RFC 5322 section 2.1.1).
49const HARD_LIMIT: usize = 998;
50/// The line length writers keep to where they can (RFC 5322 section 2.1.1).
51const SOFT_LIMIT: usize = 78;
52
53/// Skips CFWS at `v[i..]` (WSP, `CRLF WSP`, and comments, depth-limited);
54/// returns the index after it.
55pub(super) fn cfws(v: &[u8], mut i: usize, limits: &crate::Limits) -> Result<usize> {
56    loop {
57        match v.get(i) {
58            Some(b' ' | b'\t') => i += 1,
59            Some(b'\r') => {
60                if v.get(i + 1) == Some(&b'\n') && matches!(v.get(i + 2), Some(b' ' | b'\t')) {
61                    i += 3;
62                } else {
63                    return err(ErrorKind::BareCr, i);
64                }
65            }
66            Some(b'\n') => return err(ErrorKind::LineFeed, i),
67            Some(b'(') => i = crate::text::comment_end(v, i, limits)?.0,
68            _ => return Ok(i),
69        }
70    }
71}
72
73/// Checks a header field value for writing: no CR, LF, or NUL, and at most
74/// 998 bytes (the physical line limit of RFC 5322 section 2.1.1, which the
75/// caller's `Name: ` also counts toward).
76///
77/// # Errors
78///
79/// [`ErrorKind::BareCr`], [`ErrorKind::LineFeed`], [`ErrorKind::Nul`];
80/// [`ErrorKind::LineTooLong`].
81///
82/// # Examples
83///
84/// ```
85/// use parse_net::{mail, ErrorKind};
86///
87/// assert!(mail::check_header_value(b"Hello there").is_ok());
88/// assert_eq!(mail::check_header_value(b"a\r\nBcc: x").unwrap_err().kind(), ErrorKind::BareCr);
89/// assert_eq!(mail::check_header_value(&[b'a'; 999]).unwrap_err().kind(), ErrorKind::LineTooLong);
90/// ```
91pub fn check_header_value(value: &[u8]) -> Result<()> {
92    if let Some(at) = value.iter().position(|&b| matches!(b, b'\r' | b'\n' | 0)) {
93        return err(control_kind(value.get(at).copied().unwrap_or(0)), at);
94    }
95    if value.len() > HARD_LIMIT {
96        return err(ErrorKind::LineTooLong, HARD_LIMIT);
97    }
98    Ok(())
99}
100
101/// Whether `name` is an email header field name: one or more `ftext`
102/// (`%d33-57 / %d59-126`, RFC 5322 section 3.6.8).
103///
104/// # Examples
105///
106/// ```
107/// assert!(parse_net::mail::is_field_name(b"X-Mailer"));
108/// assert!(!parse_net::mail::is_field_name(b"Bad:Name") && !parse_net::mail::is_field_name(b""));
109/// ```
110#[must_use]
111pub fn is_field_name(name: &[u8]) -> bool {
112    !name.is_empty() && name.iter().all(|&b| matches!(b, 33..=57 | 59..=126))
113}
114
115/// Validates a folded header field body and returns its pieces: the text
116/// between `CRLF WSP` folds, each CRLF removed and the WSP kept (RFC 5322
117/// section 2.2.3). Concatenated, the pieces are the unfolded value.
118///
119/// # Errors
120///
121/// [`ErrorKind::BareCr`] (a CR not followed by LF and WSP),
122/// [`ErrorKind::LineFeed`] (an LF without CR), [`ErrorKind::Nul`].
123///
124/// # Examples
125///
126/// ```
127/// use parse_net::mail;
128///
129/// let u = mail::unfold(b"a long\r\n subject")?;
130/// assert_eq!(u.iter().collect::<Vec<_>>(), [&b"a long"[..], b" subject"]);
131/// assert!(mail::unfold(b"a\r\nb").is_err());
132/// # Ok::<(), parse_net::ParseError>(())
133/// ```
134pub fn unfold(value: &[u8]) -> Result<Unfolded<'_>> {
135    let mut i = 0usize;
136    while let Some(&b) = value.get(i) {
137        match b {
138            b'\r' => {
139                if value.get(i + 1) == Some(&b'\n')
140                    && matches!(value.get(i + 2), Some(b' ' | b'\t'))
141                {
142                    i += 3;
143                    continue;
144                }
145                return err(ErrorKind::BareCr, i);
146            }
147            b'\n' => return err(ErrorKind::LineFeed, i),
148            0 => return err(ErrorKind::Nul, i),
149            _ => i += 1,
150        }
151    }
152    Ok(Unfolded { value })
153}
154
155/// A validated folded field body.
156///
157/// # Examples
158///
159/// ```
160/// let u = parse_net::mail::unfold(b"x\r\n\ty")?;
161/// let joined: Vec<u8> = u.iter().flatten().copied().collect();
162/// assert_eq!(joined, b"x\ty");
163/// # Ok::<(), parse_net::ParseError>(())
164/// ```
165#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
166pub struct Unfolded<'a> {
167    value: &'a [u8],
168}
169
170impl<'a> Unfolded<'a> {
171    /// The pieces between folds, in order.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// let u = parse_net::mail::unfold(b"no folds here")?;
177    /// assert_eq!(u.iter().collect::<Vec<_>>(), [&b"no folds here"[..]]);
178    /// # Ok::<(), parse_net::ParseError>(())
179    /// ```
180    #[must_use]
181    pub fn iter(&self) -> UnfoldIter<'a> {
182        UnfoldIter {
183            rest: Some(self.value),
184        }
185    }
186}
187
188/// The pieces of an [`Unfolded`] value.
189///
190/// # Examples
191///
192/// ```
193/// let mut it = parse_net::mail::unfold(b"a\r\n b\r\n c")?.iter();
194/// assert_eq!(it.next(), Some(&b"a"[..]));
195/// assert_eq!(it.next(), Some(&b" b"[..]));
196/// assert_eq!(it.next(), Some(&b" c"[..]));
197/// assert_eq!(it.next(), None);
198/// # Ok::<(), parse_net::ParseError>(())
199/// ```
200#[derive(Clone, Debug)]
201pub struct UnfoldIter<'a> {
202    rest: Option<&'a [u8]>,
203}
204
205impl<'a> Iterator for UnfoldIter<'a> {
206    type Item = &'a [u8];
207
208    fn next(&mut self) -> Option<&'a [u8]> {
209        let rest = self.rest?;
210        match rest.iter().position(|&b| b == b'\r') {
211            Some(at) => {
212                self.rest = rest.get(at + 2..);
213                rest.get(..at)
214            }
215            None => {
216                self.rest = None;
217                Some(rest)
218            }
219        }
220    }
221}
222
223/// Writes `Name: value` with CRLF folds before whitespace so lines stay
224/// within 78 columns where the words allow, never creating a line of only
225/// whitespace and never one over 998 bytes. A space is added after the colon
226/// when `value` does not start with whitespace. No CRLF is written at the
227/// end.
228///
229/// # Errors
230///
231/// [`WriteError::NotToken`] for a name outside `ftext`;
232/// [`WriteError::Forbidden`] for CR, LF, or NUL in `value`;
233/// [`WriteError::OutOfRange`] for a word (with the whitespace that ends the
234/// value, after the last word) that cannot fit in 998 bytes;
235/// [`WriteError::Full`] (nothing written when the sink reports its room).
236///
237/// # Examples
238///
239/// ```
240/// use parse_net::{mail, SliceSink};
241///
242/// let mut buf = [0u8; 256];
243/// let mut out = SliceSink::new(&mut buf);
244/// let words = "word ".repeat(20);
245/// mail::fold("Subject", words.trim_end().as_bytes(), &mut out)?;
246/// let lines: Vec<&[u8]> = out.written().split(|&b| b == b'\n').collect();
247/// assert_eq!(lines.len(), 2);
248/// assert!(lines.iter().all(|l| l.len() <= 79));
249/// # Ok::<(), parse_net::WriteError>(())
250/// ```
251pub fn fold(name: &str, value: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
252    if !is_field_name(name.as_bytes()) {
253        return Err(WriteError::NotToken);
254    }
255    if let Some(at) = value.iter().position(|&b| matches!(b, b'\r' | b'\n' | 0)) {
256        return Err(WriteError::forbidden(at));
257    }
258    let mut count = CountSink::new();
259    put_fold(name.as_bytes(), value, &mut count)?;
260    reserve(out, count.count())?;
261    put_fold(name.as_bytes(), value, out)
262}
263
264fn put_fold(
265    name: &[u8],
266    value: &[u8],
267    out: &mut impl Sink,
268) -> core::result::Result<(), WriteError> {
269    out.put(name)?;
270    out.put(b":")?;
271    let mut col = name.len() + 1;
272    if value.first().is_some_and(|&b| b != b' ' && b != b'\t') {
273        out.put(b" ")?;
274        col += 1;
275    }
276    let mut words_on_line = 0usize;
277    let mut rest = value;
278    loop {
279        let ws_len = rest
280            .iter()
281            .take_while(|&&b| b == b' ' || b == b'\t')
282            .count();
283        let (ws, tail) = rest.split_at(ws_len);
284        let word_len = tail
285            .iter()
286            .take_while(|&&b| b != b' ' && b != b'\t')
287            .count();
288        let (word, tail) = tail.split_at(word_len);
289        if word.is_empty() {
290            // Trailing whitespace stays on the line it ends: a fold before
291            // it would leave a line of only whitespace. It counts toward
292            // the 998 bytes like any word.
293            if col + ws.len() > HARD_LIMIT {
294                return Err(WriteError::OutOfRange);
295            }
296            out.put(ws)?;
297            return Ok(());
298        }
299        if words_on_line > 0 && col + ws.len() + word.len() > SOFT_LIMIT {
300            out.put(b"\r\n")?;
301            col = 0;
302            words_on_line = 0;
303        }
304        if col + ws.len() + word.len() > HARD_LIMIT {
305            return Err(WriteError::OutOfRange);
306        }
307        out.put(ws)?;
308        out.put(word)?;
309        col += ws.len() + word.len();
310        words_on_line += 1;
311        rest = tail;
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use std::{vec, vec::Vec};
319
320    #[test]
321    fn test_check_header_value_limits() {
322        assert!(check_header_value(&[b'a'; 998]).is_ok());
323        assert_eq!(
324            check_header_value(&[b'a'; 999]).unwrap_err().kind(),
325            ErrorKind::LineTooLong
326        );
327        assert_eq!(
328            check_header_value(b"a\nb").unwrap_err().kind(),
329            ErrorKind::LineFeed
330        );
331        assert_eq!(
332            check_header_value(b"a\x00b").unwrap_err().kind(),
333            ErrorKind::Nul
334        );
335        assert!(check_header_value(b"").is_ok());
336    }
337
338    #[test]
339    fn test_field_names() {
340        assert!(is_field_name(b"Subject") && is_field_name(b"!#$%&'*+-.^_`|~9"));
341        assert!(!is_field_name(b"A B") && !is_field_name(b"A:") && !is_field_name(b"\x7f"));
342    }
343
344    #[test]
345    fn test_unfold_edge_cases() {
346        let u = unfold(b"a\r\n\tb\r\n c").unwrap();
347        assert_eq!(u.iter().collect::<Vec<_>>(), vec![&b"a"[..], b"\tb", b" c"]);
348        assert_eq!(
349            unfold(b"").unwrap().iter().collect::<Vec<_>>(),
350            vec![&b""[..]]
351        );
352        assert_eq!(unfold(b"a\r").unwrap_err().kind(), ErrorKind::BareCr);
353        assert_eq!(unfold(b"a\r\n").unwrap_err().kind(), ErrorKind::BareCr);
354        assert_eq!(unfold(b"a\nb").unwrap_err().kind(), ErrorKind::LineFeed);
355    }
356
357    fn folded(name: &str, value: &[u8]) -> Vec<u8> {
358        let mut buf = vec![0u8; 4096];
359        let mut out = crate::SliceSink::new(&mut buf);
360        fold(name, value, &mut out).unwrap();
361        out.written().to_vec()
362    }
363
364    #[test]
365    fn test_fold_lines_and_round_trip() {
366        let value = "the quick brown fox jumps over the lazy dog ".repeat(5);
367        let value = value.trim_end().as_bytes();
368        let f = folded("Subject", value);
369        for line in f.split(|&b| b == b'\n') {
370            let line = line.strip_suffix(b"\r").unwrap_or(line);
371            assert!(line.len() <= 78, "{}", line.len());
372            assert!(!line.iter().all(|&b| b == b' '));
373        }
374        let body = f.strip_prefix(b"Subject: ").unwrap();
375        let back: Vec<u8> = unfold(body).unwrap().iter().flatten().copied().collect();
376        assert_eq!(back, value);
377        assert_eq!(folded("X", b" lead"), b"X: lead");
378        assert_eq!(folded("X", b"tab\tsep"), b"X: tab\tsep");
379        assert_eq!(folded("X", b""), b"X:");
380        assert_eq!(folded("X", b"a  "), b"X: a  ");
381    }
382
383    #[test]
384    fn test_fold_long_words() {
385        let word = vec![b'w'; 989];
386        let f = folded("Subject", &word);
387        assert_eq!(f.len(), 998);
388        let mut buf = vec![0u8; 4096];
389        let mut out = crate::SliceSink::new(&mut buf);
390        assert_eq!(
391            fold("Subject", &[b'w'; 1000], &mut out),
392            Err(WriteError::OutOfRange)
393        );
394        let mut two = vec![b'a'; 10];
395        two.push(b' ');
396        two.extend_from_slice(&[b'w'; 997]);
397        assert_eq!(fold("S", &two, &mut out), Ok(()));
398        let mut two = vec![b'a'; 10];
399        two.push(b' ');
400        two.extend_from_slice(&[b'w'; 998]);
401        assert_eq!(fold("S", &two, &mut out), Err(WriteError::OutOfRange));
402        // Found by the 0.7 fuzz run: whitespace after the last word was
403        // written past 998 bytes. `Subject: ` and 989 bytes fill the line.
404        let mut tail = vec![b'w'; 989];
405        tail.push(b'\t');
406        assert_eq!(
407            fold("Subject", &tail, &mut out),
408            Err(WriteError::OutOfRange)
409        );
410        let mut fits = vec![b'w'; 988];
411        fits.push(b'\t');
412        assert_eq!(folded("Subject", &fits).len(), 998);
413        // Earlier words fold away; the last line still counts its tail.
414        let mut many = b"a b ".to_vec();
415        many.extend_from_slice(&[b'w'; 997]);
416        many.extend_from_slice(b"  ");
417        assert_eq!(fold("S", &many, &mut out), Err(WriteError::OutOfRange));
418    }
419
420    #[test]
421    fn test_fold_refusals_write_nothing() {
422        let mut buf = [0u8; 8];
423        let mut out = crate::SliceSink::new(&mut buf);
424        assert_eq!(fold("A B", b"x", &mut out), Err(WriteError::NotToken));
425        assert_eq!(
426            fold("A", b"x\r\nBcc: y", &mut out),
427            Err(WriteError::Forbidden { at: 1 })
428        );
429        assert_eq!(
430            fold("A", b"too long for eight", &mut out),
431            Err(WriteError::Full)
432        );
433        assert!(out.is_empty());
434    }
435}