Skip to main content

parse_net/mail/
phrase.rs

1//! The text of a `phrase` (RFC 5322 section 3.2.5), such as a display name,
2//! with its RFC 2047 encoded words decoded (section 5).
3
4use super::words::{self, Text};
5use crate::{
6    class,
7    error::{ErrorKind, ParseError, Result, err},
8    limits::Limits,
9    scan::{control_kind, is_control},
10    text::{comment_end_folded, parse_quoted},
11};
12
13/// Whether `b` belongs to an atom of a phrase: `atext`, the `.` of
14/// `obs-phrase`, or RFC 6532 UTF-8.
15fn is_word_byte(b: u8) -> bool {
16    WORD.get(usize::from(b)).copied().unwrap_or(false)
17}
18
19/// [`is_word_byte`] for every byte: one lookup instead of the class tests.
20// Built at compile time; `b` runs over 0..256, the array's own bounds.
21#[allow(clippy::indexing_slicing)]
22const WORD: [bool; 256] = {
23    let mut t = [false; 256];
24    let mut b = 0usize;
25    while b < 256 {
26        t[b] = class::is_atext(b as u8) || b == b'.' as usize || b >= 0x80;
27        b += 1;
28    }
29    t
30};
31
32/// The last word written.
33#[derive(Clone, Copy, PartialEq, Eq)]
34enum Last {
35    None,
36    Text,
37    Encoded,
38}
39
40/// Walks the phrase `v`, emitting its text.
41fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
42    let mut text = Text::new();
43    let (mut i, mut words) = (0usize, 0usize);
44    let mut last = Last::None;
45    // A comment since the last word: two encoded words with a comment between
46    // them are not adjacent (RFC 2047 section 6.2), so a space separates them.
47    let mut commented = false;
48    while let Some(&b) = v.get(i) {
49        let n = words::ws_len(v, i);
50        if n > 0 {
51            i += n;
52            continue;
53        }
54        if b == b'(' {
55            i = comment_end_folded(v, i, limits)?.0;
56            commented = true;
57            continue;
58        }
59        if b == b'"' {
60            let (q, used) = parse_quoted(v.get(i..).unwrap_or(&[])).map_err(|e| e.shifted(i))?;
61            if last != Last::None {
62                emit(b" ");
63            }
64            // The content with its quoted pairs unescaped; a word inside a
65            // quoted string is not decoded (RFC 2047 section 5).
66            let raw = q.raw();
67            if raw.is_ascii() {
68                // `qdtext` and `quoted-pair` hold no control but HTAB in
69                // ASCII, so the runs between escapes go out as they are.
70                let mut rest = raw;
71                while let Some(p) = rest.iter().position(|&c| c == b'\\') {
72                    emit(rest.get(..p).unwrap_or(&[]));
73                    emit(rest.get(p + 1..p + 2).unwrap_or(&[]));
74                    rest = rest.get(p + 2..).unwrap_or(&[]);
75                }
76                emit(rest);
77            } else {
78                let mut k = 0usize;
79                while let Some(&c) = raw.get(k) {
80                    if c == b'\\' {
81                        k += 1;
82                    }
83                    let c = raw.get(k).copied().unwrap_or(c);
84                    text.check(c, i + 1 + k)?;
85                    emit(&[c]);
86                    k += 1;
87                }
88                if !text.is_complete() {
89                    return err(ErrorKind::InvalidUtf8, i + used - 1);
90                }
91            }
92            last = Last::Text;
93            commented = false;
94            i += used;
95            continue;
96        }
97        if !is_word_byte(b) {
98            let kind = if is_control(b) {
99                control_kind(b)
100            } else {
101                ErrorKind::InvalidByte
102            };
103            return Err(ParseError::new(kind, i));
104        }
105        let rest = v.get(i..).unwrap_or(&[]);
106        let end = i + rest
107            .iter()
108            .position(|&c| !is_word_byte(c))
109            .unwrap_or(rest.len());
110        // An encoded word is a whole atom between whitespace (RFC 2047
111        // section 5); next to a special, a comment, or a quoted string, on
112        // either side, it is text. An atom starts at the start, or after
113        // whitespace (a fold ends in SP or HTAB), a `)`, or a `"`, so the
114        // byte before it tells which.
115        let alone = || {
116            let before = i == 0 || matches!(v.get(i - 1), Some(b' ' | b'\t'));
117            before && (end == v.len() || words::ws_len(v, end) > 0)
118        };
119        let encoded = if rest.starts_with(b"=?") && alone() {
120            words::shape(v.get(..end).unwrap_or(&[]), i)?
121        } else {
122            None
123        };
124        if let Some(e) = encoded {
125            words += 1;
126            if words > usize::from(limits.encoded_words) {
127                return err(ErrorKind::TooManyMembers, i);
128            }
129            if last == Last::Text || (last == Last::Encoded && commented) {
130                emit(b" ");
131            }
132            words::decode_word(&e, i, emit)?;
133            last = Last::Encoded;
134        } else {
135            if last != Last::None {
136                emit(b" ");
137            }
138            let atom = v.get(i..end).unwrap_or(&[]);
139            // An ASCII atom is `atext` and dots, all printable.
140            if !atom.is_ascii() {
141                for (k, &c) in atom.iter().enumerate() {
142                    text.check(c, i + k)?;
143                }
144                if !text.is_complete() {
145                    return err(ErrorKind::InvalidUtf8, end);
146                }
147            }
148            emit(atom);
149            last = Last::Text;
150        }
151        commented = false;
152        i = end;
153    }
154    if last == Last::None {
155        return err(ErrorKind::Empty, 0);
156    }
157    Ok(())
158}
159
160/// Decodes a `phrase` (RFC 5322 section 3.2.5), such as the display name
161/// [`parse_mailbox`](super::parse_mailbox) returns raw, into `out` and returns
162/// its text: comments are dropped, quoted strings lose their quotes and
163/// their quoted pairs are unescaped, and the words are joined by one space;
164/// an empty word, `""` or an encoded word with no text, still counts as one.
165/// `CRLF WSP` folds are read as whitespace between words and inside
166/// comments; a quoted string must be unfolded first
167/// ([`unfold`](super::unfold)), as in
168/// [`parse_mime_params`](super::parse_mime_params).
169/// RFC 2047 encoded words are decoded where section 5 allows them in a
170/// phrase: a whole atom with whitespace, or the start or end of the phrase,
171/// on both sides, so never inside a quoted string or against a comment;
172/// two encoded words with only whitespace between them are joined with none
173/// (section 6.2). Encoded words are read as
174/// [`decode_words`](super::decode_words) reads them, and every byte of the
175/// text meets its rules: UTF-8, no control character but HTAB.
176///
177/// # Errors
178///
179/// [`ErrorKind::Empty`] for a phrase with no word (empty, or only comments
180/// and whitespace); [`ErrorKind::InvalidByte`] for a special such as `,` or
181/// `<` outside a quoted string or comment; a control kind for a control
182/// byte, other than HTAB and the CRLF of a fold; [`ErrorKind::Control`] for
183/// a C1 character; [`ErrorKind::InvalidUtf8`];
184/// [`ErrorKind::UnterminatedQuote`]; [`ErrorKind::UnbalancedComment`],
185/// [`ErrorKind::CommentTooDeep`]; the encoded-word errors of
186/// [`decode_words`](super::decode_words), [`ErrorKind::TooManyMembers`]
187/// beyond `max_encoded_words` among them; [`ErrorKind::BufferTooSmall`].
188/// Nothing is written on error.
189///
190/// # Examples
191///
192/// ```
193/// use parse_net::{mail, Limits};
194///
195/// let m = mail::parse_mailbox(b"\"Doe, Jane\" (Sales) <jane@example.com>", &Limits::DEFAULT)?;
196/// let mut buf = [0u8; 64];
197/// let name = mail::decode_phrase(m.display_name.unwrap_or_default(), &Limits::DEFAULT, &mut buf)?;
198/// assert_eq!(name, "Doe, Jane");
199///
200/// let name = mail::decode_phrase(b"=?UTF-8?Q?Ren=C3=A9e?= \"O'Neil\"", &Limits::DEFAULT, &mut buf)?;
201/// assert_eq!(name, "Ren\u{e9}e O'Neil");
202/// # Ok::<(), parse_net::ParseError>(())
203/// ```
204pub fn decode_phrase<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
205    let mut len = 0usize;
206    walk(value, limits, &mut |b: &[u8]| len += b.len())?;
207    let Some(dst) = out.get_mut(..len) else {
208        return err(ErrorKind::BufferTooSmall, 0);
209    };
210    let mut n = 0usize;
211    walk(value, limits, &mut |b: &[u8]| {
212        if let Some(d) = dst.get_mut(n..n + b.len()) {
213            d.copy_from_slice(b);
214        }
215        n += b.len();
216    })?;
217    // The first walk checked the text; this is the safe conversion.
218    core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use std::{string::String, vec, vec::Vec};
225
226    fn dec(v: &[u8]) -> Result<String> {
227        let mut buf = vec![0u8; 1024];
228        decode_phrase(v, &Limits::DEFAULT, &mut buf).map(String::from)
229    }
230
231    fn fails(v: &[u8]) -> (ErrorKind, usize) {
232        let e = dec(v).unwrap_err();
233        (e.kind(), e.offset())
234    }
235
236    #[test]
237    fn test_decode_phrase_words() {
238        assert_eq!(dec(b"Jane Doe").unwrap(), "Jane Doe");
239        assert_eq!(dec(b"  Jane \t  Doe  ").unwrap(), "Jane Doe");
240        assert_eq!(dec(b"Jane\r\n Doe").unwrap(), "Jane Doe");
241        assert_eq!(dec(b"John Q. Public").unwrap(), "John Q. Public");
242        assert_eq!(dec(b"\"Doe, John\"").unwrap(), "Doe, John");
243        assert_eq!(dec(b"\"a \\\"q\\\" \\\\ b\"").unwrap(), "a \"q\" \\ b");
244        assert_eq!(dec(b"Jane\"Q\"Doe").unwrap(), "Jane Q Doe");
245        assert_eq!(dec(b"\"  spaced  \" x").unwrap(), "  spaced   x");
246        assert_eq!(dec(b"\"\"").unwrap(), "");
247        assert_eq!(dec(b"John (the (nested) one) Doe").unwrap(), "John Doe");
248        assert_eq!(dec(b"(lead)John(trail)").unwrap(), "John");
249        assert_eq!(
250            dec("J\u{f6}rg M\u{fc}ller".as_bytes()).unwrap(),
251            "J\u{f6}rg M\u{fc}ller"
252        );
253        assert_eq!(
254            dec("\"\u{e9}t\u{e9}\"".as_bytes()).unwrap(),
255            "\u{e9}t\u{e9}"
256        );
257        assert_eq!(dec(b"\"tab\there\"").unwrap(), "tab\there");
258    }
259
260    #[test]
261    fn test_decode_phrase_encoded_words() {
262        assert_eq!(
263            dec(b"=?UTF-8?Q?Caf=C3=A9?= Owner").unwrap(),
264            "Caf\u{e9} Owner"
265        );
266        assert_eq!(
267            dec(b"Owner =?utf-8?q?Caf=C3=A9?=").unwrap(),
268            "Owner Caf\u{e9}"
269        );
270        assert_eq!(dec(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=").unwrap(), "ab");
271        assert_eq!(dec(b"=?UTF-8?Q?a?=\r\n =?UTF-8?Q?b?=").unwrap(), "ab");
272        assert_eq!(dec(b"=?UTF-8?Q?a?= (c) =?UTF-8?Q?b?=").unwrap(), "a b");
273        assert_eq!(dec(b"=?UTF-8?Q?a_b?=").unwrap(), "a b");
274        assert_eq!(
275            dec(b"=?ISO-8859-1?Q?Andr=E9?= Pirard").unwrap(),
276            "Andr\u{e9} Pirard"
277        );
278        assert_eq!(
279            dec(b"=?UTF-8?B?5Lya6K2w44Gu44GK55+l44KJ44Gb?=").unwrap(),
280            "\u{4f1a}\u{8b70}\u{306e}\u{304a}\u{77e5}\u{3089}\u{305b}"
281        );
282        // Inside a quoted string, or next to a special, it is text.
283        assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "=?UTF-8?Q?a?=");
284        assert_eq!(dec(b"=?UTF-8?Q?a?=(c)").unwrap(), "=?UTF-8?Q?a?=");
285        assert_eq!(dec(b"=?UTF-8?Q?a?=\"b\"").unwrap(), "=?UTF-8?Q?a?= b");
286        // The same on the side before the word.
287        assert_eq!(dec(b"(c)=?UTF-8?Q?a?=").unwrap(), "=?UTF-8?Q?a?=");
288        assert_eq!(dec(b"\"x\"=?UTF-8?Q?a?=").unwrap(), "x =?UTF-8?Q?a?=");
289        assert_eq!(dec(b"(c) =?UTF-8?Q?a?=").unwrap(), "a");
290        assert_eq!(dec(b"\"x\" =?UTF-8?Q?a?=").unwrap(), "x a");
291        assert_eq!(
292            dec(b"=?UTF-8?Q?a?= (c)=?UTF-8?Q?b?=").unwrap(),
293            "a =?UTF-8?Q?b?="
294        );
295        // An empty encoded word is an empty word, as an empty quoted string is.
296        assert_eq!(dec(b"=?UTF-8?B??=").unwrap(), "");
297        assert_eq!(dec(b"x =?UTF-8?Q??= y").unwrap(), "x  y");
298        assert_eq!(dec(b"x \"\" y").unwrap(), "x  y");
299        // Not the shape of an encoded word: text.
300        assert_eq!(dec(b"=?x").unwrap(), "=?x");
301        assert_eq!(dec(b"a=?UTF-8?Q?b?=").unwrap(), "a=?UTF-8?Q?b?=");
302    }
303
304    #[test]
305    fn test_decode_phrase_errors() {
306        assert_eq!(fails(b""), (ErrorKind::Empty, 0));
307        assert_eq!(fails(b"   "), (ErrorKind::Empty, 0));
308        assert_eq!(fails(b"(only a comment)"), (ErrorKind::Empty, 0));
309        assert_eq!(fails(b"Doe, John"), (ErrorKind::InvalidByte, 3));
310        assert_eq!(fails(b"a <b>"), (ErrorKind::InvalidByte, 2));
311        assert_eq!(fails(b"a@b"), (ErrorKind::InvalidByte, 1));
312        assert_eq!(fails(b"a\x01b").0, ErrorKind::Control);
313        assert_eq!(fails(b"a\rb").0, ErrorKind::BareCr);
314        assert_eq!(fails(b"a\nb").0, ErrorKind::LineFeed);
315        assert_eq!(fails(b"a\0b").0, ErrorKind::Nul);
316        assert_eq!(fails(b"\"open"), (ErrorKind::UnterminatedQuote, 5));
317        assert_eq!(fails(b"a (open").0, ErrorKind::UnbalancedComment);
318        assert_eq!(fails(b"a \xff"), (ErrorKind::InvalidUtf8, 2));
319        assert_eq!(fails(b"a \xc3 b"), (ErrorKind::InvalidUtf8, 3));
320        assert_eq!(fails(b"\"\xc3\" b"), (ErrorKind::InvalidUtf8, 2));
321        assert_eq!(fails(b"a\xc2\x85b"), (ErrorKind::Control, 2));
322        assert_eq!(fails(b"\"\\\xc2\\\x85\"").0, ErrorKind::Control);
323        assert_eq!(fails(b"=?KOI8-R?Q?a?=").0, ErrorKind::UnsupportedCharset);
324        assert_eq!(fails(b"x =?UTF-8?Q?=0D?=").0, ErrorKind::ControlInDecoded);
325        assert_eq!(fails(b"=?UTF-8?B?YQ=?=").0, ErrorKind::InvalidEncodedWord);
326        let l = Limits::DEFAULT.max_encoded_words(1);
327        let mut buf = [0u8; 16];
328        assert_eq!(
329            decode_phrase(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf)
330                .unwrap_err()
331                .kind(),
332            ErrorKind::TooManyMembers
333        );
334        // Nothing is written when the buffer is short.
335        let mut small = [b'#'; 4];
336        assert_eq!(
337            decode_phrase(b"Jane Doe", &Limits::DEFAULT, &mut small)
338                .unwrap_err()
339                .kind(),
340            ErrorKind::BufferTooSmall
341        );
342        assert_eq!(small, [b'#'; 4]);
343    }
344
345    /// A reader with no shortcuts: words made only of atoms, quoted
346    /// strings, whitespace, and comments without nesting, so the expected
347    /// text is the words joined by one space.
348    fn reference(words: &[(bool, String)]) -> String {
349        let mut out = String::new();
350        for (k, (_, w)) in words.iter().enumerate() {
351            if k > 0 {
352                out.push(' ');
353            }
354            out.push_str(w);
355        }
356        out
357    }
358
359    proptest::proptest! {
360        #[test]
361        fn decode_phrase_matches_reference(
362            words in proptest::collection::vec(
363                (proptest::bool::ANY, "[ -~\u{e9}\u{4f1a}]{0,8}", "[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.\u{e9}-]{1,8}"),
364                1..6,
365            ),
366            gaps in proptest::collection::vec(0usize..4, 6),
367        ) {
368            let mut raw = Vec::new();
369            let mut expect = Vec::new();
370            for (k, (quoted, q, atom)) in words.iter().enumerate() {
371                if k > 0 {
372                    match gaps.get(k).copied().unwrap_or(0) {
373                        0 => raw.push(b' '),
374                        1 => raw.extend_from_slice(b"\t "),
375                        2 => raw.extend_from_slice(b" (note) "),
376                        _ => raw.extend_from_slice(b"\r\n "),
377                    }
378                }
379                if *quoted {
380                    raw.push(b'"');
381                    for c in q.bytes() {
382                        if c == b'"' || c == b'\\' {
383                            raw.push(b'\\');
384                        }
385                        raw.push(c);
386                    }
387                    raw.push(b'"');
388                    expect.push((true, q.clone()));
389                } else {
390                    raw.extend_from_slice(atom.as_bytes());
391                    expect.push((false, atom.clone()));
392                }
393            }
394            // Atoms that happen to look like encoded words decode: keep
395            // to the plain case here.
396            proptest::prop_assume!(!expect.iter().any(|(q, w)| !q && w.starts_with("=?")));
397            proptest::prop_assert_eq!(dec(&raw).unwrap(), reference(&expect));
398        }
399
400        #[test]
401        fn decode_phrase_never_panics(v in proptest::collection::vec(proptest::num::u8::ANY, 0..64)) {
402            let mut buf = [0u8; 256];
403            if let Ok(s) = decode_phrase(&v, &Limits::DEFAULT, &mut buf) {
404                proptest::prop_assert!(!s.chars().any(|c| c.is_control() && c != '\t'));
405            }
406        }
407    }
408}