email_encoding/headers/
rfc2231.rs

1//! [RFC 2231] encoder.
2//!
3//! [RFC 2231]: https://datatracker.ietf.org/doc/html/rfc2231
4
5use std::fmt::{self, Write};
6
7use super::{hex_encoding, utils, writer::EmailWriter, MAX_LINE_LEN};
8
9/// Encode a string via RFC 2231.
10///
11/// # Examples
12///
13/// ```rust
14/// # use email_encoding::headers::writer::EmailWriter;
15/// # fn main() -> std::fmt::Result {
16/// {
17///     let input = "invoice.pdf";
18///
19///     let mut output = String::new();
20///     {
21///         let mut writer = EmailWriter::new(&mut output, 0, 0, false);
22///         email_encoding::headers::rfc2231::encode("filename", input, &mut writer)?;
23///     }
24///     assert_eq!(output, "filename=\"invoice.pdf\"");
25/// }
26///
27/// {
28///     let input = "invoice_2022_06_04_letshaveaverylongfilenamewhynotemailcanhandleit.pdf";
29///
30///     let mut output = String::new();
31///     {
32///         let mut writer = EmailWriter::new(&mut output, 0, 0, false);
33///         email_encoding::headers::rfc2231::encode("filename", input, &mut writer)?;
34///     }
35///     assert_eq!(
36///         output,
37///         concat!(
38///             "\r\n",
39///             " filename*0=\"invoice_2022_06_04_letshaveaverylongfilenamewhynotemailcanha\";\r\n",
40///             " filename*1=\"ndleit.pdf\""
41///         )
42///     );
43/// }
44///
45/// {
46///     let input = "faktΓΊra.pdf";
47///
48///     let mut output = String::new();
49///     {
50///         let mut writer = EmailWriter::new(&mut output, 0, 0, false);
51///         email_encoding::headers::rfc2231::encode("filename", input, &mut writer)?;
52///     }
53///     assert_eq!(
54///         output,
55///         concat!(
56///             "\r\n",
57///             " filename*0*=utf-8''fakt%C3%BAra.pdf"
58///         )
59///     );
60/// }
61/// # Ok(())
62/// # }
63/// ```
64pub fn encode(key: &str, mut value: &str, w: &mut EmailWriter<'_>) -> fmt::Result {
65    assert!(
66        utils::str_is_ascii_alphanumeric(key),
67        "`key` must only be composed of ascii alphanumeric chars"
68    );
69    assert!(
70        key.len() + "*12*=utf-8'';".len() < MAX_LINE_LEN,
71        "`key` must not be too long to cause the encoder to overflow the max line length"
72    );
73
74    if utils::str_is_ascii_printable(value) {
75        // Can be written normally (Parameter Value Continuations)
76
77        let quoted_plain_combined_len = key.len() + "=\"".len() + value.len() + "\"\r\n".len();
78        if w.line_len() + quoted_plain_combined_len <= MAX_LINE_LEN {
79            // Fits line
80
81            w.write_str(key)?;
82
83            w.write_char('=')?;
84
85            w.write_char('"')?;
86            utils::write_escaped(value, w)?;
87            w.write_char('"')?;
88        } else {
89            // Doesn't fit line
90
91            w.new_line()?;
92            w.forget_spaces();
93
94            let mut i = 0_usize;
95            loop {
96                write!(w, " {}*{}=\"", key, i)?;
97
98                let remaining_len = MAX_LINE_LEN - w.line_len() - "\"\r\n".len();
99
100                let value_ =
101                    utils::truncate_to_char_boundary(value, remaining_len.min(value.len()));
102                value = &value[value_.len()..];
103
104                utils::write_escaped(value_, w)?;
105
106                w.write_char('"')?;
107
108                if value.is_empty() {
109                    // End of value
110                    break;
111                }
112
113                // End of line
114                w.write_char(';')?;
115                w.new_line()?;
116
117                i += 1;
118            }
119        }
120    } else {
121        // Needs encoding (Parameter Value Character Set and Language Information)
122
123        w.new_line()?;
124        w.forget_spaces();
125
126        let mut i = 0_usize;
127        loop {
128            write!(w, " {}*{}*=", key, i)?;
129
130            if i == 0 {
131                w.write_str("utf-8''")?;
132            }
133
134            let mut chars = value.chars();
135            while w.line_len() < MAX_LINE_LEN - "=xx=xx=xx=xx;\r\n".len() {
136                match chars.next() {
137                    Some(c) => {
138                        hex_encoding::percent_encode_char(w, c)?;
139                        value = chars.as_str();
140                    }
141                    None => {
142                        break;
143                    }
144                }
145            }
146
147            if value.is_empty() {
148                // End of value
149                break;
150            }
151
152            // End of line
153            w.write_char(';')?;
154            w.new_line()?;
155
156            i += 1;
157        }
158    }
159
160    Ok(())
161}
162
163#[cfg(test)]
164mod tests {
165    use pretty_assertions::assert_eq;
166
167    use super::*;
168
169    #[test]
170    fn empty() {
171        let mut s = "Content-Disposition: attachment;".to_string();
172        let line_len = 1;
173
174        {
175            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
176            w.space();
177            encode("filename", "", &mut w).unwrap();
178        }
179
180        assert_eq!(s, concat!("Content-Disposition: attachment; filename=\"\""));
181    }
182
183    #[test]
184    fn parameter() {
185        let mut s = "Content-Disposition: attachment;".to_string();
186        let line_len = 1;
187
188        {
189            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
190            w.space();
191            encode("filename", "duck.txt", &mut w).unwrap();
192        }
193
194        assert_eq!(
195            s,
196            concat!("Content-Disposition: attachment; filename=\"duck.txt\"")
197        );
198    }
199
200    #[test]
201    fn parameter_to_escape() {
202        let mut s = "Content-Disposition: attachment;".to_string();
203        let line_len = 1;
204
205        {
206            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
207            w.space();
208            encode("filename", "du\"ck\\.txt", &mut w).unwrap();
209        }
210
211        assert_eq!(
212            s,
213            concat!("Content-Disposition: attachment; filename=\"du\\\"ck\\\\.txt\"")
214        );
215    }
216
217    #[test]
218    fn parameter_long() {
219        let mut s = "Content-Disposition: attachment;".to_string();
220        let line_len = s.len();
221
222        {
223            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
224            w.space();
225            encode(
226                "filename",
227                "a-fairly-long-filename-just-to-see-what-happens-when-we-encode-it-will-the-client-be-able-to-handle-it.txt",
228                &mut w,
229            )
230            .unwrap();
231        }
232
233        assert_eq!(
234            s,
235            concat!(
236                "Content-Disposition: attachment;\r\n",
237                " filename*0=\"a-fairly-long-filename-just-to-see-what-happens-when-we-enco\";\r\n",
238                " filename*1=\"de-it-will-the-client-be-able-to-handle-it.txt\""
239            )
240        );
241    }
242
243    #[test]
244    fn parameter_special() {
245        let mut s = "Content-Disposition: attachment;".to_string();
246        let line_len = s.len();
247
248        {
249            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
250            w.space();
251            encode("filename", "caffè.txt", &mut w).unwrap();
252        }
253
254        assert_eq!(
255            s,
256            concat!(
257                "Content-Disposition: attachment;\r\n",
258                " filename*0*=utf-8''caff%C3%A8.txt"
259            )
260        );
261    }
262
263    #[test]
264    fn parameter_special_long() {
265        let mut s = "Content-Disposition: attachment;".to_string();
266        let line_len = s.len();
267
268        {
269            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
270            w.space();
271            encode(
272                "filename",
273                "testing-to-see-what-happens-when-πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•-are-placed-on-the-boundary.txt",
274                &mut w,
275            )
276            .unwrap();
277        }
278
279        assert_eq!(
280            s,
281            concat!(
282                "Content-Disposition: attachment;\r\n",
283                " filename*0*=utf-8''testing-to-see-what-happens-when-%F0%9F%93%95;\r\n",
284                " filename*1*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
285                " filename*2*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
286                " filename*3*=%F0%9F%93%95%F0%9F%93%95-are-placed-on-the-bound;\r\n",
287                " filename*4*=ary.txt"
288            )
289        );
290    }
291
292    #[test]
293    fn parameter_special_long_part2() {
294        let mut s = "Content-Disposition: attachment;".to_string();
295        let line_len = s.len();
296
297        {
298            let mut w = EmailWriter::new(&mut s, line_len, 0, true);
299            w.space();
300            encode(
301                "filename",
302                "testing-to-see-what-happens-when-books-are-placed-in-the-second-part-πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•πŸ“•.txt",
303                &mut w,
304            )
305            .unwrap();
306        }
307
308        assert_eq!(
309            s,
310            concat!(
311                "Content-Disposition: attachment;\r\n",
312                " filename*0*=utf-8''testing-to-see-what-happens-when-books-ar;\r\n",
313                " filename*1*=e-placed-in-the-second-part-%F0%9F%93%95%F0%9F%93%95;\r\n",
314                " filename*2*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
315                " filename*3*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
316                " filename*4*=%F0%9F%93%95.txt"
317            )
318        );
319    }
320
321    #[test]
322    fn parameter_dont_split_on_hex_boundary() {
323        let base_header = "Content-Disposition: attachment;".to_string();
324        let line_len = base_header.len();
325
326        for start_offset in &["", "x", "xx", "xxx"] {
327            let mut filename = start_offset.to_string();
328
329            for i in 1..256 {
330                // 'Ü' results in two hex chars %C3%9C
331                filename.push('Ü');
332
333                let mut output = base_header.clone();
334                {
335                    let mut w = EmailWriter::new(&mut output, line_len, 0, true);
336                    encode("filename", &filename, &mut w).unwrap();
337                }
338
339                // look for all hex encoded chars
340                let output_len = output.len();
341                let mut found_hex_count = 0;
342                for (percent_sign_idx, _) in output.match_indices('%') {
343                    assert!(percent_sign_idx + 3 <= output_len);
344
345                    // verify we get the expected hex sequence for an 'Ü'
346                    let must_be_hex = &output[percent_sign_idx + 1..percent_sign_idx + 3];
347                    assert!(
348                        must_be_hex == "C3" || must_be_hex == "9C",
349                        "unexpected hex char: {}",
350                        must_be_hex
351                    );
352                    found_hex_count += 1;
353                }
354                // verify the number of hex encoded chars adds up
355                let number_of_chars_in_hex = 2;
356                assert_eq!(found_hex_count, i * number_of_chars_in_hex);
357
358                // verify max line length
359                let mut last_newline_pos = 0;
360                for (newline_idx, _) in output.match_indices("\r\n") {
361                    let line_length = newline_idx - last_newline_pos;
362                    assert!(
363                        line_length < MAX_LINE_LEN,
364                        "expected line length exceeded: {} > {}",
365                        line_length,
366                        MAX_LINE_LEN
367                    );
368                    last_newline_pos = newline_idx;
369                }
370                // ensure there was at least one newline
371                assert_ne!(0, last_newline_pos);
372            }
373        }
374    }
375
376    #[test]
377    #[should_panic(expected = "`key` must only be composed of ascii alphanumeric chars")]
378    fn non_ascii_key() {
379        let mut s = String::new();
380        let mut w = EmailWriter::new(&mut s, 0, 0, true);
381        let _ = encode("πŸ“¬", "", &mut w);
382    }
383}