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 that cannot fit in 998 bytes;
234/// [`WriteError::Full`] (nothing written when the sink reports its room).
235///
236/// # Examples
237///
238/// ```
239/// use parse_net::{mail, SliceSink};
240///
241/// let mut buf = [0u8; 256];
242/// let mut out = SliceSink::new(&mut buf);
243/// let words = "word ".repeat(20);
244/// mail::fold("Subject", words.trim_end().as_bytes(), &mut out)?;
245/// let lines: Vec<&[u8]> = out.written().split(|&b| b == b'\n').collect();
246/// assert_eq!(lines.len(), 2);
247/// assert!(lines.iter().all(|l| l.len() <= 79));
248/// # Ok::<(), parse_net::WriteError>(())
249/// ```
250pub fn fold(name: &str, value: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
251    if !is_field_name(name.as_bytes()) {
252        return Err(WriteError::NotToken);
253    }
254    if let Some(at) = value.iter().position(|&b| matches!(b, b'\r' | b'\n' | 0)) {
255        return Err(WriteError::forbidden(at));
256    }
257    let mut count = CountSink::new();
258    put_fold(name.as_bytes(), value, &mut count)?;
259    reserve(out, count.count())?;
260    put_fold(name.as_bytes(), value, out)
261}
262
263fn put_fold(
264    name: &[u8],
265    value: &[u8],
266    out: &mut impl Sink,
267) -> core::result::Result<(), WriteError> {
268    out.put(name)?;
269    out.put(b":")?;
270    let mut col = name.len() + 1;
271    if value.first().is_some_and(|&b| b != b' ' && b != b'\t') {
272        out.put(b" ")?;
273        col += 1;
274    }
275    let mut words_on_line = 0usize;
276    let mut rest = value;
277    loop {
278        let ws_len = rest
279            .iter()
280            .take_while(|&&b| b == b' ' || b == b'\t')
281            .count();
282        let (ws, tail) = rest.split_at(ws_len);
283        let word_len = tail
284            .iter()
285            .take_while(|&&b| b != b' ' && b != b'\t')
286            .count();
287        let (word, tail) = tail.split_at(word_len);
288        if word.is_empty() {
289            // Trailing whitespace stays on the line it ends.
290            out.put(ws)?;
291            return Ok(());
292        }
293        if words_on_line > 0 && col + ws.len() + word.len() > SOFT_LIMIT {
294            out.put(b"\r\n")?;
295            col = 0;
296            words_on_line = 0;
297        }
298        if col + ws.len() + word.len() > HARD_LIMIT {
299            return Err(WriteError::OutOfRange);
300        }
301        out.put(ws)?;
302        out.put(word)?;
303        col += ws.len() + word.len();
304        words_on_line += 1;
305        rest = tail;
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use std::{vec, vec::Vec};
313
314    #[test]
315    fn test_check_header_value_limits() {
316        assert!(check_header_value(&[b'a'; 998]).is_ok());
317        assert_eq!(
318            check_header_value(&[b'a'; 999]).unwrap_err().kind(),
319            ErrorKind::LineTooLong
320        );
321        assert_eq!(
322            check_header_value(b"a\nb").unwrap_err().kind(),
323            ErrorKind::LineFeed
324        );
325        assert_eq!(
326            check_header_value(b"a\x00b").unwrap_err().kind(),
327            ErrorKind::Nul
328        );
329        assert!(check_header_value(b"").is_ok());
330    }
331
332    #[test]
333    fn test_field_names() {
334        assert!(is_field_name(b"Subject") && is_field_name(b"!#$%&'*+-.^_`|~9"));
335        assert!(!is_field_name(b"A B") && !is_field_name(b"A:") && !is_field_name(b"\x7f"));
336    }
337
338    #[test]
339    fn test_unfold_edge_cases() {
340        let u = unfold(b"a\r\n\tb\r\n c").unwrap();
341        assert_eq!(u.iter().collect::<Vec<_>>(), vec![&b"a"[..], b"\tb", b" c"]);
342        assert_eq!(
343            unfold(b"").unwrap().iter().collect::<Vec<_>>(),
344            vec![&b""[..]]
345        );
346        assert_eq!(unfold(b"a\r").unwrap_err().kind(), ErrorKind::BareCr);
347        assert_eq!(unfold(b"a\r\n").unwrap_err().kind(), ErrorKind::BareCr);
348        assert_eq!(unfold(b"a\nb").unwrap_err().kind(), ErrorKind::LineFeed);
349    }
350
351    fn folded(name: &str, value: &[u8]) -> Vec<u8> {
352        let mut buf = vec![0u8; 4096];
353        let mut out = crate::SliceSink::new(&mut buf);
354        fold(name, value, &mut out).unwrap();
355        out.written().to_vec()
356    }
357
358    #[test]
359    fn test_fold_lines_and_round_trip() {
360        let value = "the quick brown fox jumps over the lazy dog ".repeat(5);
361        let value = value.trim_end().as_bytes();
362        let f = folded("Subject", value);
363        for line in f.split(|&b| b == b'\n') {
364            let line = line.strip_suffix(b"\r").unwrap_or(line);
365            assert!(line.len() <= 78, "{}", line.len());
366            assert!(!line.iter().all(|&b| b == b' '));
367        }
368        let body = f.strip_prefix(b"Subject: ").unwrap();
369        let back: Vec<u8> = unfold(body).unwrap().iter().flatten().copied().collect();
370        assert_eq!(back, value);
371        assert_eq!(folded("X", b" lead"), b"X: lead");
372        assert_eq!(folded("X", b"tab\tsep"), b"X: tab\tsep");
373        assert_eq!(folded("X", b""), b"X:");
374        assert_eq!(folded("X", b"a  "), b"X: a  ");
375    }
376
377    #[test]
378    fn test_fold_long_words() {
379        let word = vec![b'w'; 989];
380        let f = folded("Subject", &word);
381        assert_eq!(f.len(), 998);
382        let mut buf = vec![0u8; 4096];
383        let mut out = crate::SliceSink::new(&mut buf);
384        assert_eq!(
385            fold("Subject", &[b'w'; 1000], &mut out),
386            Err(WriteError::OutOfRange)
387        );
388        let mut two = vec![b'a'; 10];
389        two.push(b' ');
390        two.extend_from_slice(&[b'w'; 997]);
391        assert_eq!(fold("S", &two, &mut out), Ok(()));
392        let mut two = vec![b'a'; 10];
393        two.push(b' ');
394        two.extend_from_slice(&[b'w'; 998]);
395        assert_eq!(fold("S", &two, &mut out), Err(WriteError::OutOfRange));
396    }
397
398    #[test]
399    fn test_fold_refusals_write_nothing() {
400        let mut buf = [0u8; 8];
401        let mut out = crate::SliceSink::new(&mut buf);
402        assert_eq!(fold("A B", b"x", &mut out), Err(WriteError::NotToken));
403        assert_eq!(
404            fold("A", b"x\r\nBcc: y", &mut out),
405            Err(WriteError::Forbidden { at: 1 })
406        );
407        assert_eq!(
408            fold("A", b"too long for eight", &mut out),
409            Err(WriteError::Full)
410        );
411        assert!(out.is_empty());
412    }
413}