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