email_encoding/headers/
rfc2047.rs

1//! [RFC 2047] encoder.
2//!
3//! [RFC 2047]: https://datatracker.ietf.org/doc/html/rfc2047
4
5use core::fmt::{self, Write};
6
7use super::{utils, writer::EmailWriter, MAX_LINE_LEN};
8
9const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
10const ENCODING_END_SUFFIX: &str = "?=";
11
12/// Encode a string via RFC 2047.
13///
14/// # Examples
15///
16/// ```rust
17/// # use email_encoding::headers::writer::EmailWriter;
18/// # fn main() -> core::fmt::Result {
19/// let input = "Adrián";
20///
21/// let mut output = String::new();
22/// {
23///     let mut writer = EmailWriter::new(&mut output, 0, 0, false);
24///     email_encoding::headers::rfc2047::encode(input, &mut writer)?;
25/// }
26/// assert_eq!(output, "=?utf-8?b?QWRyacOhbg==?=");
27/// # Ok(())
28/// # }
29/// ```
30pub fn encode(mut s: &str, w: &mut EmailWriter<'_>) -> fmt::Result {
31    let mut wrote = false;
32
33    while !s.is_empty() {
34        let remaining_line_len = MAX_LINE_LEN.saturating_sub(
35            ENCODING_START_PREFIX.len() + ENCODING_END_SUFFIX.len() + w.line_len() + "\r\n".len(),
36        );
37        let unencoded_remaining_line_len = remaining_line_len / 4 * 3;
38
39        let mut word =
40            utils::truncate_to_char_boundary(s, unencoded_remaining_line_len.min(s.len()));
41        if word.is_empty() {
42            if wrote || w.has_spaces() {
43                // No space remaining on this line, go to a new one
44                w.new_line()?;
45                if !w.has_spaces() {
46                    // The last write before this call to `encode` most
47                    // likely wasn't rfc2047 so we must write a "soft"
48                    // space to let the decoder know we're still within the
49                    // same header
50                    w.space();
51                }
52                continue;
53            }
54
55            // No space remaining, but going to a new line will require us
56            // to introduce a new space, which will mess up things even more.
57            word = &s[..s.chars().next().expect("`s` is empty").len_utf8()];
58        }
59
60        // Write the prefix
61        w.write_str(ENCODING_START_PREFIX)?;
62
63        // Encode `word`
64        let encoder = base64::display::Base64Display::new(
65            word.as_bytes(),
66            &base64::engine::general_purpose::STANDARD,
67        );
68        write!(w, "{}", encoder)?;
69
70        // Write the suffix
71        w.write_str(ENCODING_END_SUFFIX)?;
72
73        s = &s[word.len()..];
74        wrote = true;
75    }
76
77    Ok(())
78}
79
80#[cfg(test)]
81mod tests {
82    use alloc::string::String;
83
84    use pretty_assertions::assert_eq;
85
86    use super::*;
87
88    #[test]
89    fn empty() {
90        let mut s = String::new();
91        let line_len = s.len();
92
93        {
94            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
95            encode("", &mut w).unwrap();
96        }
97
98        assert_eq!(s, "");
99    }
100
101    #[test]
102    fn basic() {
103        let mut s = String::new();
104        let line_len = s.len();
105
106        {
107            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
108            encode("abcd", &mut w).unwrap();
109        }
110
111        assert_eq!(s, "=?utf-8?b?YWJjZA==?=");
112    }
113
114    #[test]
115    fn basic_nopad() {
116        let mut s = String::new();
117        let line_len = s.len();
118
119        {
120            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
121            encode("abcdef", &mut w).unwrap();
122        }
123
124        assert_eq!(s, "=?utf-8?b?YWJjZGVm?=");
125    }
126
127    #[test]
128    fn long() {
129        let mut s = String::new();
130        let line_len = s.len();
131
132        {
133            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
134            encode(&"lettre".repeat(20), &mut w).unwrap();
135        }
136
137        assert_eq!(
138            s,
139            concat!(
140                "=?utf-8?b?bGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0?=\r\n",
141                " =?utf-8?b?dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJl?=\r\n",
142                " =?utf-8?b?bGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJl?="
143            )
144        );
145    }
146
147    #[test]
148    fn long_encoded() {
149        let mut s = String::new();
150        let line_len = s.len();
151
152        {
153            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
154            encode(&"hétérogénéité".repeat(16), &mut w).unwrap();
155        }
156
157        assert_eq!(
158            s,
159            concat!(
160                "=?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n",
161                " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n",
162                " =?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n",
163                " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n",
164                " =?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n",
165                " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n",
166                " =?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOp?=",
167            )
168        );
169    }
170}