email_encoding/headers/
rfc2047.rs

1//! [RFC 2047] encoder.
2//!
3//! [RFC 2047]: https://datatracker.ietf.org/doc/html/rfc2047
4
5use std::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() -> std::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 pretty_assertions::assert_eq;
83
84    use super::*;
85
86    #[test]
87    fn empty() {
88        let mut s = String::new();
89        let line_len = s.len();
90
91        {
92            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
93            encode("", &mut w).unwrap();
94        }
95
96        assert_eq!(s, "");
97    }
98
99    #[test]
100    fn basic() {
101        let mut s = String::new();
102        let line_len = s.len();
103
104        {
105            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
106            encode("abcd", &mut w).unwrap();
107        }
108
109        assert_eq!(s, "=?utf-8?b?YWJjZA==?=");
110    }
111
112    #[test]
113    fn basic_nopad() {
114        let mut s = String::new();
115        let line_len = s.len();
116
117        {
118            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
119            encode("abcdef", &mut w).unwrap();
120        }
121
122        assert_eq!(s, "=?utf-8?b?YWJjZGVm?=");
123    }
124
125    #[test]
126    fn long() {
127        let mut s = String::new();
128        let line_len = s.len();
129
130        {
131            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
132            encode(&"lettre".repeat(20), &mut w).unwrap();
133        }
134
135        assert_eq!(
136            s,
137            concat!(
138                "=?utf-8?b?bGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0?=\r\n",
139                " =?utf-8?b?dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJl?=\r\n",
140                " =?utf-8?b?bGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJlbGV0dHJl?="
141            )
142        );
143    }
144
145    #[test]
146    fn long_encoded() {
147        let mut s = String::new();
148        let line_len = s.len();
149
150        {
151            let mut w = EmailWriter::new(&mut s, line_len, 0, false);
152            encode(&"hétérogénéité".repeat(16), &mut w).unwrap();
153        }
154
155        assert_eq!(
156            s,
157            concat!(
158                "=?utf-8?b?aMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9n?=\r\n",
159                " =?utf-8?b?w6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOpaMOpdMOpcm9nw6luw6lpdMOp?=\r\n",
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?aMOpdMOpcm9nw6luw6lpdMOp?=",
165            )
166        );
167    }
168}