Skip to main content

mail_builder/encoders/
quoted_printable.rs

1/*
2 * Copyright Stalwart Labs Ltd. See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12use std::io::{self, Write};
13
14pub fn quoted_printable_encode(
15    input: &[u8],
16    mut output: impl Write,
17    is_inline: bool,
18    is_body: bool,
19) -> io::Result<usize> {
20    let mut bytes_written = 0;
21    if !is_inline {
22        if is_body {
23            let mut prev_ch = 0;
24            for (pos, &ch) in input.iter().enumerate() {
25                if ch == b'='
26                    || ch >= 127
27                    || ((ch == b' ' || ch == b'\t')
28                        && (matches!(input.get(pos + 1..), Some([b'\n', ..] | [b'\r', b'\n', ..]))
29                            || (pos == input.len() - 1)))
30                {
31                    if bytes_written + 3 > 76 {
32                        output.write_all(b"=\r\n")?;
33                        bytes_written = 0;
34                    }
35                    output.write_all(format!("={:02X}", ch).as_bytes())?;
36                    bytes_written += 3;
37                } else if ch == b'\n' {
38                    if prev_ch != b'\r' {
39                        output.write_all(b"\r\n")?;
40                    } else {
41                        output.write_all(b"\n")?;
42                    }
43                    bytes_written = 0;
44                } else {
45                    prev_ch = ch;
46                    if bytes_written + 1 > 76 {
47                        output.write_all(b"=\r\n")?;
48                        bytes_written = 0;
49                    }
50                    output.write_all(&[ch])?;
51                    bytes_written += 1;
52                }
53            }
54        } else {
55            for (pos, &ch) in input.iter().enumerate() {
56                if ch == b'='
57                    || ch >= 127
58                    || (ch == b'\r' || ch == b'\n')
59                    || ((ch == b' ' || ch == b'\t') && (pos == input.len() - 1))
60                {
61                    if bytes_written + 3 > 76 {
62                        output.write_all(b"=\r\n")?;
63                        bytes_written = 0;
64                    }
65                    output.write_all(format!("={:02X}", ch).as_bytes())?;
66                    bytes_written += 3;
67                } else {
68                    if bytes_written + 1 > 76 {
69                        output.write_all(b"=\r\n")?;
70                        bytes_written = 0;
71                    }
72                    output.write_all(&[ch])?;
73                    bytes_written += 1;
74                }
75            }
76        }
77    } else {
78        for &ch in input.iter() {
79            if ch == b'=' || ch == b'?' || ch == b'\t' || ch == b'\r' || ch == b'\n' || ch >= 127 {
80                output.write_all(format!("={:02X}", ch).as_bytes())?;
81                bytes_written += 3;
82            } else if ch == b' ' {
83                output.write_all(b"_")?;
84                bytes_written += 1;
85            } else {
86                output.write_all(&[ch])?;
87                bytes_written += 1;
88            }
89        }
90    }
91
92    Ok(bytes_written)
93}
94
95#[cfg(test)]
96mod tests {
97
98    #[test]
99    fn encode_quoted_printable() {
100        for (input, expected_result_body, expected_result_attachment, expected_result_inline) in [
101            ("hello world".to_string(), "hello world", "hello world", "hello_world"),
102            (
103                "hello ? world ?".to_string(),
104                "hello ? world ?",
105                "hello ? world ?",
106                "hello_=3F_world_=3F",
107            ),
108            (
109                "hello = world =".to_string(),
110                "hello =3D world =3D",
111                "hello =3D world =3D",
112                "hello_=3D_world_=3D",
113            ),
114            (
115                "hello\nworld\n".to_string(),
116                "hello\r\nworld\r\n",
117                "hello=0Aworld=0A",
118                "hello=0Aworld=0A",
119            ),
120            (
121                "hello   \nworld   \r\n   ".to_string(),
122                "hello  =20\r\nworld  =20\r\n  =20",
123                "hello   =0Aworld   =0D=0A  =20",
124                "hello___=0Aworld___=0D=0A___",
125            ),
126            (
127                "hello   \nworld   \n".to_string(),
128                "hello  =20\r\nworld  =20\r\n",
129                "hello   =0Aworld   =0A",
130                "hello___=0Aworld___=0A",
131            ),
132            (
133                "áéíóú".to_string(),
134                "=C3=A1=C3=A9=C3=AD=C3=B3=C3=BA",
135                "=C3=A1=C3=A9=C3=AD=C3=B3=C3=BA",
136                "=C3=A1=C3=A9=C3=AD=C3=B3=C3=BA",
137            ),
138            (
139                "안녕하세요 세계".to_string(),
140                "=EC=95=88=EB=85=95=ED=95=98=EC=84=B8=EC=9A=94 =EC=84=B8=EA=B3=84",
141                "=EC=95=88=EB=85=95=ED=95=98=EC=84=B8=EC=9A=94 =EC=84=B8=EA=B3=84",
142                "=EC=95=88=EB=85=95=ED=95=98=EC=84=B8=EC=9A=94_=EC=84=B8=EA=B3=84",
143            ),
144            (
145                " ".repeat(100),
146                concat!(
147                    "                                            ",
148                    "                                =\r\n    ",
149                    "                   =20"
150                ),
151                concat!(
152                    "                                            ",
153                    "                                =\r\n    ",
154                    "                   =20"
155                ),
156                concat!(
157                    "_________________________________________",
158                    "_____________________________________________",
159                    "______________"
160                ),
161            ),
162        ] {
163            let mut output = Vec::new();
164            super::quoted_printable_encode(input.as_bytes(), &mut output, false, true).unwrap();
165            assert_eq!(std::str::from_utf8(&output).unwrap(), expected_result_body, "body");
166
167            let mut output = Vec::new();
168            super::quoted_printable_encode(input.as_bytes(), &mut output, false, false).unwrap();
169            assert_eq!(std::str::from_utf8(&output).unwrap(), expected_result_attachment, "attachment");
170
171            let mut output = Vec::new();
172            super::quoted_printable_encode(input.as_bytes(), &mut output, true, false).unwrap();
173            assert_eq!(std::str::from_utf8(&output).unwrap(), expected_result_inline, "inline");
174        }
175    }
176}