Skip to main content

mail_parser/decoders/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! Decoders the parser uses that are also useful on their own: charset
8//! conversion, HTML to text and text to HTML, and body previews. Transfer
9//! encodings (base64, quoted-printable, hex) are decoded by encodify; use
10//! it directly for those, or [`crate::Encoding::decode`] to decode a body
11//! as the parser does.
12
13pub mod charsets;
14pub mod html;
15mod prefix;
16pub mod preview;
17
18use crate::Encoding;
19use encodify::{base64, qp};
20use std::borrow::Cow;
21
22pub(crate) use prefix::{Limit, TextPrefix, truncate};
23
24impl Encoding {
25    pub(crate) fn parse(value: &[u8]) -> Option<Encoding> {
26        let token = value
27            .split(|&byte| byte == b'(')
28            .next()
29            .unwrap_or_default()
30            .trim_ascii();
31        hashify::map_ignore_case!(token, Encoding,
32            "7bit" => Encoding::None,
33            "8bit" => Encoding::None,
34            "binary" => Encoding::None,
35            "base64" => Encoding::Base64,
36            "quoted-printable" => Encoding::QuotedPrintable,
37        )
38        .copied()
39    }
40
41    /// Decodes a body in this Content-Transfer-Encoding, as [`crate::MessagePart::decoded`] does.
42    pub fn decode(self, bytes: &[u8]) -> Cow<'_, [u8]> {
43        match self {
44            Encoding::None => Cow::Borrowed(bytes),
45            Encoding::QuotedPrintable => qp::BODY.decode(bytes).unwrap_or(Cow::Borrowed(bytes)),
46            Encoding::Base64 => {
47                let mut out = Vec::new();
48                self.decode_append(bytes, &mut out);
49                Cow::Owned(out)
50            }
51        }
52    }
53
54    /// Decodes a body like [`Encoding::decode`] and reports whether the encoding was malformed.
55    pub fn decode_checked(self, bytes: &[u8]) -> (Cow<'_, [u8]>, bool) {
56        match self {
57            Encoding::None => (Cow::Borrowed(bytes), false),
58            Encoding::QuotedPrintable => match qp::BODY.strict().decode(bytes) {
59                Ok(decoded) => (decoded, false),
60                Err(_) => (self.decode(bytes), true),
61            },
62            Encoding::Base64 => {
63                let mut out = Vec::new();
64                let malformed = base64::MIME.decode_append(bytes, &mut out).is_err();
65                if malformed {
66                    base64_tolerant(bytes, &mut out);
67                }
68                (Cow::Owned(out), malformed)
69            }
70        }
71    }
72
73    /// Decodes a body like [`Encoding::decode`], appending the result to `out`.
74    pub fn decode_append(self, bytes: &[u8], out: &mut Vec<u8>) {
75        match self {
76            Encoding::None => out.extend_from_slice(bytes),
77            Encoding::QuotedPrintable => {
78                if qp::BODY.decode_append(bytes, out).is_err() {
79                    out.extend_from_slice(bytes);
80                }
81            }
82            Encoding::Base64 => {
83                if base64::MIME.decode_append(bytes, out).is_err() {
84                    base64_tolerant(bytes, out);
85                }
86            }
87        }
88    }
89
90    /// Returns the length of a body after transfer decoding, as [`crate::MessagePart::decoded_len`] does.
91    pub fn decoded_len(self, bytes: &[u8]) -> usize {
92        match self {
93            Encoding::None => bytes.len(),
94            Encoding::Base64 => base64::MIME.decoded_len(bytes).unwrap_or_else(|_| {
95                let mut out = Vec::new();
96                base64_tolerant(bytes, &mut out);
97                out.len()
98            }),
99            Encoding::QuotedPrintable => qp::BODY.decoded_len(bytes).unwrap_or(bytes.len()),
100        }
101    }
102}
103
104fn base64_tolerant(rest: &[u8], out: &mut Vec<u8>) {
105    base64_feed(rest, out).finish(out);
106}
107
108fn base64_feed(mut rest: &[u8], out: &mut Vec<u8>) -> base64::Decoder {
109    let mut decoder = base64::MIME.decoder();
110    loop {
111        rest = rest
112            .get(decoder.decode_symbols(rest, out)..)
113            .unwrap_or_default();
114        match rest.split_first() {
115            None => return decoder,
116            Some((b'=', tail)) => {
117                decoder.pad(out);
118                rest = tail;
119            }
120            Some((_, tail)) => rest = tail,
121        }
122    }
123}
124
125pub(crate) fn rewrite<'x>(
126    text: Cow<'x, str>,
127    rewrite: impl for<'a> FnOnce(&'a str) -> Cow<'a, str>,
128) -> Cow<'x, str> {
129    match text {
130        Cow::Borrowed(text) => rewrite(text),
131        Cow::Owned(text) => {
132            let rewritten = match rewrite(&text) {
133                Cow::Owned(rewritten) => Some(rewritten),
134                Cow::Borrowed(same) if same.len() == text.len() => None,
135                Cow::Borrowed(other) => Some(other.to_owned()),
136            };
137            Cow::Owned(rewritten.unwrap_or(text))
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::{Charset, scan::tests::Rng};
146
147    #[test]
148    fn qp_matches_codec() {
149        let alphabet = b"ab \t\r\n=\xc3\xa9";
150        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
151        for _ in 0..20_000 {
152            let len = rng.below(24);
153            let input: Vec<u8> = (0..len)
154                .map(|_| {
155                    alphabet
156                        .get(rng.below(alphabet.len()))
157                        .copied()
158                        .unwrap_or(b'a')
159                })
160                .collect();
161            let mut decoded = Vec::new();
162            qp::BODY
163                .decode_append(&input, &mut decoded)
164                .expect("never fails");
165            let borrowed = Encoding::QuotedPrintable.decode(&input);
166            assert_eq!(borrowed.as_ref(), decoded.as_slice(), "{input:?}");
167            if matches!(borrowed, Cow::Borrowed(_)) {
168                assert_eq!(decoded, input);
169            }
170            assert_eq!(Encoding::QuotedPrintable.decoded_len(&input), decoded.len());
171        }
172        assert!(matches!(
173            Encoding::QuotedPrintable.decode(b"plain text\r\nline two\nend"),
174            Cow::Borrowed(_)
175        ));
176        assert!(matches!(
177            Encoding::QuotedPrintable.decode(b"trailing \r\n"),
178            Cow::Owned(_)
179        ));
180    }
181
182    #[test]
183    fn base64_best_effort() {
184        assert_eq!(Encoding::Base64.decode(b"SGVs\r\nbG8=").as_ref(), b"Hello");
185        assert_eq!(Encoding::Base64.decode(b"SGVs-bG8=").as_ref(), b"Hello");
186        assert_eq!(Encoding::Base64.decode(b"VGVzdA").as_ref(), b"Test");
187        assert_eq!(Encoding::Base64.decoded_len(b"SGVs-bG8="), 5);
188        assert_eq!(Encoding::Base64.decoded_len(b"SGVsbG8="), 5);
189    }
190
191    #[test]
192    fn checked_transfer_decoding() {
193        let alphabet = b"ab \t\r\n=\xc3\xa9AZ09+/-";
194        let mut rng = Rng(0x7ab5_c0de_1234_9876);
195        for _ in 0..20_000 {
196            let len = rng.below(24);
197            let input: Vec<u8> = (0..len)
198                .map(|_| {
199                    alphabet
200                        .get(rng.below(alphabet.len()))
201                        .copied()
202                        .unwrap_or(b'a')
203                })
204                .collect();
205            for encoding in [Encoding::None, Encoding::QuotedPrintable, Encoding::Base64] {
206                let (checked, malformed) = encoding.decode_checked(&input);
207                assert_eq!(checked, encoding.decode(&input), "{input:?}");
208                let expected = match encoding {
209                    Encoding::None => false,
210                    Encoding::QuotedPrintable => qp::BODY.strict().decoded_len(&input).is_err(),
211                    Encoding::Base64 => base64::MIME.decoded_len(&input).is_err(),
212                };
213                assert_eq!(malformed, expected, "{encoding:?} {input:?}");
214            }
215        }
216        for (input, encoding, decoded, malformed) in [
217            (&b"SGVs\r\nbG8="[..], Encoding::Base64, &b"Hello"[..], false),
218            (b"SGVs-bG8=", Encoding::Base64, b"Hello", true),
219            (b"VGVzdA", Encoding::Base64, b"Test", false),
220            (
221                b"caf=C3=A9",
222                Encoding::QuotedPrintable,
223                b"caf\xc3\xa9",
224                false,
225            ),
226            (
227                b"soft=\r\nbreak",
228                Encoding::QuotedPrintable,
229                b"softbreak",
230                false,
231            ),
232            (b"a=ZZb", Encoding::QuotedPrintable, b"a=ZZb", true),
233            (b"end=4", Encoding::QuotedPrintable, b"end=4", true),
234            (b"as is=", Encoding::None, b"as is=", false),
235        ] {
236            let (checked, problem) = encoding.decode_checked(input);
237            assert_eq!(
238                (checked.as_ref(), problem),
239                (decoded, malformed),
240                "{input:?}"
241            );
242        }
243    }
244
245    #[test]
246    fn known_transfer_encodings() {
247        for (value, encoding) in [
248            (&b"7bit"[..], Encoding::None),
249            (b" 8BIT\r\n", Encoding::None),
250            (b"binary (comment)", Encoding::None),
251            (b"\r\n base64\r\n", Encoding::Base64),
252            (b" BASE64 (comment)\n", Encoding::Base64),
253            (b" Quoted-Printable", Encoding::QuotedPrintable),
254        ] {
255            assert_eq!(Encoding::parse(value), Some(encoding), "{value:?}");
256        }
257        for value in [
258            &b"x-uuencode"[..],
259            b"",
260            b" base64;",
261            b"7-bit",
262            b"\"base64\"",
263        ] {
264            assert_eq!(Encoding::parse(value), None, "{value:?}");
265        }
266    }
267
268    #[test]
269    fn text_conversion() {
270        let label = |label: &str| Charset::from_label(label.as_bytes()).unwrap_or_default();
271        assert!(matches!(
272            label("utf-8").decode_cow(Cow::Borrowed(b"caf\xc3\xa9")),
273            Cow::Borrowed("café")
274        ));
275        assert_eq!(
276            label("iso-8859-1").decode_cow(Cow::Borrowed(b"caf\xe9")),
277            "café"
278        );
279        assert_eq!(
280            Charset::default().decode_cow(Cow::Owned(b"caf\xe9".to_vec())),
281            "caf\u{fffd}"
282        );
283        assert_eq!(
284            label("x-unknown").decode_cow(Cow::Owned(b"caf\xe9".to_vec())),
285            "caf\u{fffd}"
286        );
287    }
288}