Skip to main content

parse_net/mail/
words.rs

1//! RFC 2047 encoded words in unstructured header text.
2
3use crate::{
4    class,
5    error::{ErrorKind, ParseError, Result, WriteError, err},
6    limits::Limits,
7    utf8::Utf8,
8    write::{CountSink, Sink, reserve},
9};
10
11/// The longest encoded word (RFC 2047 section 2).
12const WORD_LIMIT: usize = 75;
13/// `=?UTF-8?Q?` and `?=`.
14const OVERHEAD: usize = 12;
15
16/// Whether `b` is written as itself inside a Q-encoded word in a header
17/// (RFC 2047 section 5, rule 3).
18const fn q_plain(b: u8) -> bool {
19    b.is_ascii_alphanumeric() || matches!(b, b'!' | b'*' | b'+' | b'-' | b'/')
20}
21
22/// The length of `c` in Q encoding.
23fn q_len(c: char) -> usize {
24    let mut buf = [0u8; 4];
25    c.encode_utf8(&mut buf)
26        .bytes()
27        .map(|b| if q_plain(b) || b == b' ' { 1 } else { 3 })
28        .sum()
29}
30
31fn put_q(c: char, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
32    const HEX: &[u8; 16] = b"0123456789ABCDEF";
33    let mut buf = [0u8; 4];
34    for b in c.encode_utf8(&mut buf).bytes() {
35        if b == b' ' {
36            out.put(b"_")?;
37        } else if q_plain(b) {
38            out.put(&[b])?;
39        } else {
40            let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
41            let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
42            out.put(&[b'=', hi, lo])?;
43        }
44    }
45    Ok(())
46}
47
48fn put_words(
49    text: &str,
50    room_first_line: usize,
51    out: &mut impl Sink,
52) -> core::result::Result<(), WriteError> {
53    if text.bytes().all(|b| (0x20..=0x7e).contains(&b)) && !text.contains("=?") {
54        out.put(text.as_bytes())?;
55        return Ok(());
56    }
57    let mut limit = room_first_line.min(WORD_LIMIT);
58    let mut payload = 0usize;
59    let mut open = false;
60    for c in text.chars() {
61        let n = q_len(c);
62        if open && OVERHEAD + payload + n > limit {
63            out.put(b"?=\r\n ")?;
64            open = false;
65            limit = WORD_LIMIT;
66        }
67        if !open {
68            if OVERHEAD + n > limit {
69                // Not even one character fits in the room left on the first
70                // line: start on a new one.
71                out.put(b"\r\n ")?;
72                limit = WORD_LIMIT;
73            }
74            out.put(b"=?UTF-8?Q?")?;
75            open = true;
76            payload = 0;
77        }
78        put_q(c, out)?;
79        payload += n;
80    }
81    if open {
82        out.put(b"?=")?;
83    }
84    Ok(())
85}
86
87/// Writes `text` for an unstructured header field: as it is when it is
88/// printable ASCII without `=?`, otherwise as RFC 2047 `=?UTF-8?Q?...?=`
89/// words of at most 75 bytes, split only between characters, separated by
90/// CRLF SP. The first word fits in `room_first_line` (the columns left after
91/// `Name: `); when not even one character fits there, the words start on a
92/// new line.
93///
94/// # Errors
95///
96/// [`WriteError::Forbidden`] for a control character (C0, DEL, or C1);
97/// [`WriteError::Full`] (nothing written when the sink reports its room).
98///
99/// # Examples
100///
101/// ```
102/// use parse_net::{mail, SliceSink};
103///
104/// let mut buf = [0u8; 256];
105/// let mut out = SliceSink::new(&mut buf);
106/// mail::encode_words("Caf\u{e9} au lait", 64, &mut out).expect("room");
107/// assert_eq!(out.written(), b"=?UTF-8?Q?Caf=C3=A9_au_lait?=");
108/// ```
109pub fn encode_words(
110    text: &str,
111    room_first_line: usize,
112    out: &mut impl Sink,
113) -> core::result::Result<(), WriteError> {
114    if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
115        return Err(WriteError::forbidden(at));
116    }
117    let mut count = CountSink::new();
118    put_words(text, room_first_line, &mut count)?;
119    reserve(out, count.count())?;
120    put_words(text, room_first_line, out)
121}
122
123/// The charsets decoded.
124#[derive(Clone, Copy, PartialEq, Eq)]
125enum Charset {
126    Utf8,
127    Ascii,
128    Latin1,
129}
130
131/// The parts of an encoded word.
132struct Encoded<'a> {
133    charset: Charset,
134    /// The `B` encoding.
135    base64: bool,
136    text: &'a [u8],
137    /// The index after the closing `?=`.
138    end: usize,
139}
140
141/// An encoded word at `v[i..]`: `=?charset?enc?text?=`. `Ok(None)` when the
142/// bytes do not have that shape (they are then ordinary text).
143fn shape(v: &[u8], i: usize) -> Result<Option<Encoded<'_>>> {
144    let rest = v.get(i + 2..).unwrap_or(&[]);
145    let Some(q1) = rest.iter().position(|&b| b == b'?') else {
146        return Ok(None);
147    };
148    let charset = rest.get(..q1).unwrap_or(&[]);
149    if charset.is_empty()
150        || !charset
151            .iter()
152            .all(|&b| class::is_mime_token_char(b) && b != b'?')
153    {
154        return Ok(None);
155    }
156    let after = rest.get(q1 + 1..).unwrap_or(&[]);
157    let (Some(&enc), Some(b'?')) = (after.first(), after.get(1)) else {
158        return Ok(None);
159    };
160    let text_start = i + 2 + q1 + 3;
161    let text_rest = v.get(text_start..).unwrap_or(&[]);
162    let Some(end) = text_rest.windows(2).position(|w| w == b"?=") else {
163        return Ok(None);
164    };
165    let text = text_rest.get(..end).unwrap_or(&[]);
166    if text.iter().any(|&b| b == b' ' || b == b'\t' || b == b'?') {
167        return Ok(None);
168    }
169    // RFC 2231 section 5: `charset*language`.
170    let name = charset.split(|&b| b == b'*').next().unwrap_or(&[]);
171    let cs = if name.eq_ignore_ascii_case(b"utf-8") {
172        Charset::Utf8
173    } else if name.eq_ignore_ascii_case(b"us-ascii") {
174        Charset::Ascii
175    } else if name.eq_ignore_ascii_case(b"iso-8859-1") {
176        Charset::Latin1
177    } else {
178        return err(ErrorKind::UnsupportedCharset, i + 2);
179    };
180    let b = match enc {
181        b'Q' | b'q' => false,
182        b'B' | b'b' => true,
183        _ => return err(ErrorKind::InvalidEncodedWord, i + 2 + q1 + 1),
184    };
185    Ok(Some(Encoded {
186        charset: cs,
187        base64: b,
188        text,
189        end: text_start + end + 2,
190    }))
191}
192
193/// Decodes one Q word's text, emitting UTF-8.
194fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
195    let mut utf8 = Utf8::new();
196    let mut i = 0usize;
197    while let Some(&b) = text.get(i) {
198        let byte = match b {
199            b'_' => b' ',
200            b'=' => {
201                let hi = text.get(i + 1).copied().and_then(class::hex_value);
202                let lo = text.get(i + 2).copied().and_then(class::hex_value);
203                let (Some(hi), Some(lo)) = (hi, lo) else {
204                    return err(ErrorKind::InvalidEncodedWord, at + i);
205                };
206                i += 2;
207                (hi << 4) | lo
208            }
209            0x21..=0x7e => b,
210            _ => return err(ErrorKind::InvalidEncodedWord, at + i),
211        };
212        i += 1;
213        if matches!(byte, b'\r' | b'\n' | 0) {
214            return err(ErrorKind::ControlInDecoded, at + i - 1);
215        }
216        match cs {
217            Charset::Utf8 => {
218                if !utf8.push(byte) {
219                    return err(ErrorKind::InvalidUtf8, at + i - 1);
220                }
221                emit(&[byte]);
222            }
223            Charset::Ascii if byte >= 0x80 => {
224                return err(ErrorKind::InvalidEncodedWord, at + i - 1);
225            }
226            Charset::Ascii => emit(&[byte]),
227            Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
228            Charset::Latin1 => emit(&[byte]),
229        }
230    }
231    if !utf8.is_complete() {
232        // Each encoded word holds whole characters (RFC 2047 section 5).
233        return err(ErrorKind::InvalidUtf8, at + text.len());
234    }
235    Ok(())
236}
237
238/// Walks `v`, emitting the decoded text.
239fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
240    let (mut i, mut words) = (0usize, 0usize);
241    let (mut boundary, mut quoted, mut after_word) = (true, false, false);
242    let mut held: Option<(usize, usize)> = None;
243    while let Some(&b) = v.get(i) {
244        if quoted {
245            emit(&[b]);
246            if b == b'\\' {
247                if let Some(&n) = v.get(i + 1) {
248                    emit(&[n]);
249                    i += 1;
250                }
251            } else if b == b'"' {
252                quoted = false;
253            }
254            i += 1;
255            continue;
256        }
257        if b == b' ' || b == b'\t' {
258            let start = i;
259            while matches!(v.get(i), Some(b' ' | b'\t')) {
260                i += 1;
261            }
262            if after_word {
263                held = Some((start, i));
264            } else {
265                emit(v.get(start..i).unwrap_or(&[]));
266            }
267            boundary = true;
268            continue;
269        }
270        if boundary && v.get(i..i + 2) == Some(b"=?") {
271            if let Some(Encoded {
272                charset: cs,
273                base64: b64,
274                text,
275                end,
276            }) = shape(v, i)?
277            {
278                if end == v.len() || matches!(v.get(end), Some(b' ' | b'\t')) {
279                    words += 1;
280                    if words > usize::from(limits.encoded_words) {
281                        return err(ErrorKind::TooManyMembers, i);
282                    }
283                    if b64 {
284                        // Base64 decoding comes with encode-lib (D-002).
285                        return err(ErrorKind::InvalidEncodedWord, i);
286                    }
287                    // Whitespace between two encoded words is dropped
288                    // (RFC 2047 section 6.2).
289                    held = None;
290                    let text_at = end - 2 - text.len();
291                    decode_q(cs, text, text_at, emit)?;
292                    after_word = true;
293                    boundary = false;
294                    i = end;
295                    continue;
296                }
297            }
298        }
299        if let Some((s, e)) = held.take() {
300            emit(v.get(s..e).unwrap_or(&[]));
301        }
302        after_word = false;
303        boundary = false;
304        if b == b'"' {
305            quoted = true;
306        }
307        emit(&[b]);
308        i += 1;
309    }
310    if let Some((s, e)) = held {
311        emit(v.get(s..e).unwrap_or(&[]));
312    }
313    Ok(())
314}
315
316/// Decodes the RFC 2047 encoded words in unstructured header text into
317/// `out` and returns the text. Text outside encoded words is copied;
318/// whitespace between two adjacent encoded words is dropped (section 6.2);
319/// `_` is a space in Q; an encoded word must stand alone between whitespace,
320/// and words inside a quoted string are not decoded (section 5). Charsets:
321/// UTF-8, US-ASCII, ISO-8859-1 (transcoded), with an optional RFC 2231
322/// `*language`. `B` words need the `encode-lib` feature, which is not yet
323/// available (D-002).
324///
325/// # Errors
326///
327/// [`ErrorKind::UnsupportedCharset`]; [`ErrorKind::InvalidEncodedWord`] (a
328/// bad encoding letter, a broken `=XX`, a byte not allowed in a Q word, a
329/// `B` word); [`ErrorKind::ControlInDecoded`] for a decoded CR, LF, or NUL;
330/// [`ErrorKind::InvalidUtf8`] (a UTF-8 word that is not whole characters, or
331/// text outside the words that is not UTF-8); [`ErrorKind::TooManyMembers`]
332/// beyond `max_encoded_words`; [`ErrorKind::BufferTooSmall`]. Nothing is
333/// written on error.
334///
335/// # Examples
336///
337/// ```
338/// use parse_net::{mail, Limits};
339///
340/// let mut buf = [0u8; 64];
341/// let s = mail::decode_words(b"=?utf-8?q?Caf=C3=A9?= =?ISO-8859-1?Q?_cr=E8me?= now", &Limits::DEFAULT, &mut buf)?;
342/// assert_eq!(s, "Caf\u{e9} cr\u{e8}me now");
343/// # Ok::<(), parse_net::ParseError>(())
344/// ```
345pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
346    let mut len = 0usize;
347    walk(value, limits, &mut |b: &[u8]| len += b.len())?;
348    let Some(dst) = out.get_mut(..len) else {
349        return err(ErrorKind::BufferTooSmall, 0);
350    };
351    let mut n = 0usize;
352    walk(value, limits, &mut |b: &[u8]| {
353        if let Some(d) = dst.get_mut(n..n + b.len()) {
354            d.copy_from_slice(b);
355        }
356        n += b.len();
357    })?;
358    if let Err(e) = core::str::from_utf8(dst) {
359        let at = e.valid_up_to();
360        // Nothing is left behind on error.
361        dst.fill(0);
362        return Err(ParseError::new(ErrorKind::InvalidUtf8, at));
363    }
364    core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use std::{string::String, vec};
371
372    fn enc(text: &str, room: usize) -> String {
373        let mut buf = vec![0u8; 2048];
374        let mut out = crate::SliceSink::new(&mut buf);
375        encode_words(text, room, &mut out).unwrap();
376        String::from_utf8(out.written().to_vec()).unwrap()
377    }
378
379    fn dec(v: &[u8]) -> Result<String> {
380        let mut buf = vec![0u8; 2048];
381        decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
382    }
383
384    #[test]
385    fn test_encode_words_edge_cases() {
386        assert_eq!(enc("plain ascii", 10), "plain ascii");
387        assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
388        let long = "\u{e9}".repeat(40);
389        let e = enc(&long, 75);
390        for word in e.split("\r\n ") {
391            assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
392            // Each word holds whole characters, so it decodes on its own.
393            assert!(dec(word.as_bytes()).is_ok());
394        }
395        assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
396        // An encoded word of exactly 75 bytes: 63 bytes of payload.
397        let e = enc(&"\u{20ac}".repeat(7), 75);
398        assert_eq!(e.len(), 12 + 63);
399        assert!(!e.starts_with("\r\n "));
400        assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
401        let mut buf = [0u8; 8];
402        let mut out = crate::SliceSink::new(&mut buf);
403        assert_eq!(
404            encode_words("a\nb", 70, &mut out),
405            Err(WriteError::Forbidden { at: 1 })
406        );
407        assert_eq!(
408            encode_words("a\u{85}", 70, &mut out),
409            Err(WriteError::Forbidden { at: 1 })
410        );
411        assert_eq!(
412            encode_words("\u{e9}\u{e9}", 70, &mut out),
413            Err(WriteError::Full)
414        );
415        assert!(out.is_empty());
416    }
417
418    #[test]
419    fn test_decode_words_edge_cases() {
420        assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
421        assert_eq!(dec(b"=?UTF-8?Q?a?=  \t =?UTF-8?Q?b?=").unwrap(), "ab");
422        assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
423        assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
424        assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
425        assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
426        assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
427        assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
428        assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
429        assert_eq!(
430            dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
431            ErrorKind::UnsupportedCharset
432        );
433        assert_eq!(
434            dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
435            ErrorKind::InvalidEncodedWord
436        );
437        assert_eq!(
438            dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
439            ErrorKind::InvalidEncodedWord
440        );
441        assert_eq!(
442            dec(b"=?UTF-8?B?YQ==?=").unwrap_err().kind(),
443            ErrorKind::InvalidEncodedWord
444        );
445        assert_eq!(
446            dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
447            ErrorKind::ControlInDecoded
448        );
449        assert_eq!(
450            dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
451            ErrorKind::InvalidUtf8
452        );
453        assert_eq!(
454            dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
455            ErrorKind::InvalidEncodedWord
456        );
457        assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
458        let l = Limits::DEFAULT.max_encoded_words(2);
459        let mut buf = [0u8; 64];
460        assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
461        assert_eq!(
462            decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
463                .unwrap_err()
464                .kind(),
465            ErrorKind::TooManyMembers
466        );
467        let mut small = [0u8; 2];
468        assert_eq!(
469            decode_words(b"abc", &Limits::DEFAULT, &mut small)
470                .unwrap_err()
471                .kind(),
472            ErrorKind::BufferTooSmall
473        );
474        assert_eq!(small, [0, 0]);
475    }
476}