1use core::fmt::{self, Write};
6
7use super::{hex_encoding, utils, writer::EmailWriter, MAX_LINE_LEN};
8
9pub 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 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 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 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 break;
111 }
112
113 w.write_char(';')?;
115 w.new_line()?;
116
117 i += 1;
118 }
119 }
120 } else {
121 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 break;
150 }
151
152 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 alloc::{borrow::ToOwned, string::String};
166
167 use pretty_assertions::assert_eq;
168
169 use super::*;
170
171 #[test]
172 fn empty() {
173 let mut s = "Content-Disposition: attachment;".to_owned();
174 let line_len = 1;
175
176 {
177 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
178 w.space();
179 encode("filename", "", &mut w).unwrap();
180 }
181
182 assert_eq!(s, concat!("Content-Disposition: attachment; filename=\"\""));
183 }
184
185 #[test]
186 fn parameter() {
187 let mut s = "Content-Disposition: attachment;".to_owned();
188 let line_len = 1;
189
190 {
191 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
192 w.space();
193 encode("filename", "duck.txt", &mut w).unwrap();
194 }
195
196 assert_eq!(
197 s,
198 concat!("Content-Disposition: attachment; filename=\"duck.txt\"")
199 );
200 }
201
202 #[test]
203 fn parameter_to_escape() {
204 let mut s = "Content-Disposition: attachment;".to_owned();
205 let line_len = 1;
206
207 {
208 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
209 w.space();
210 encode("filename", "du\"ck\\.txt", &mut w).unwrap();
211 }
212
213 assert_eq!(
214 s,
215 concat!("Content-Disposition: attachment; filename=\"du\\\"ck\\\\.txt\"")
216 );
217 }
218
219 #[test]
220 fn parameter_long() {
221 let mut s = "Content-Disposition: attachment;".to_owned();
222 let line_len = s.len();
223
224 {
225 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
226 w.space();
227 encode(
228 "filename",
229 "a-fairly-long-filename-just-to-see-what-happens-when-we-encode-it-will-the-client-be-able-to-handle-it.txt",
230 &mut w,
231 )
232 .unwrap();
233 }
234
235 assert_eq!(
236 s,
237 concat!(
238 "Content-Disposition: attachment;\r\n",
239 " filename*0=\"a-fairly-long-filename-just-to-see-what-happens-when-we-enco\";\r\n",
240 " filename*1=\"de-it-will-the-client-be-able-to-handle-it.txt\""
241 )
242 );
243 }
244
245 #[test]
246 fn parameter_special() {
247 let mut s = "Content-Disposition: attachment;".to_owned();
248 let line_len = s.len();
249
250 {
251 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
252 w.space();
253 encode("filename", "caffè.txt", &mut w).unwrap();
254 }
255
256 assert_eq!(
257 s,
258 concat!(
259 "Content-Disposition: attachment;\r\n",
260 " filename*0*=utf-8''caff%C3%A8.txt"
261 )
262 );
263 }
264
265 #[test]
266 fn parameter_special_long() {
267 let mut s = "Content-Disposition: attachment;".to_owned();
268 let line_len = s.len();
269
270 {
271 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
272 w.space();
273 encode(
274 "filename",
275 "testing-to-see-what-happens-when-πππππππππππ-are-placed-on-the-boundary.txt",
276 &mut w,
277 )
278 .unwrap();
279 }
280
281 assert_eq!(
282 s,
283 concat!(
284 "Content-Disposition: attachment;\r\n",
285 " filename*0*=utf-8''testing-to-see-what-happens-when-%F0%9F%93%95;\r\n",
286 " filename*1*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
287 " filename*2*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
288 " filename*3*=%F0%9F%93%95%F0%9F%93%95-are-placed-on-the-bound;\r\n",
289 " filename*4*=ary.txt"
290 )
291 );
292 }
293
294 #[test]
295 fn parameter_special_long_part2() {
296 let mut s = "Content-Disposition: attachment;".to_owned();
297 let line_len = s.len();
298
299 {
300 let mut w = EmailWriter::new(&mut s, line_len, 0, true);
301 w.space();
302 encode(
303 "filename",
304 "testing-to-see-what-happens-when-books-are-placed-in-the-second-part-πππππππππππ.txt",
305 &mut w,
306 )
307 .unwrap();
308 }
309
310 assert_eq!(
311 s,
312 concat!(
313 "Content-Disposition: attachment;\r\n",
314 " filename*0*=utf-8''testing-to-see-what-happens-when-books-ar;\r\n",
315 " filename*1*=e-placed-in-the-second-part-%F0%9F%93%95%F0%9F%93%95;\r\n",
316 " filename*2*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
317 " filename*3*=%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95%F0%9F%93%95;\r\n",
318 " filename*4*=%F0%9F%93%95.txt"
319 )
320 );
321 }
322
323 #[test]
324 fn parameter_dont_split_on_hex_boundary() {
325 let base_header = "Content-Disposition: attachment;".to_owned();
326 let line_len = base_header.len();
327
328 for start_offset in &["", "x", "xx", "xxx"] {
329 let mut filename = (*start_offset).to_owned();
330
331 for i in 1..256 {
332 filename.push('Γ');
334
335 let mut output = base_header.clone();
336 {
337 let mut w = EmailWriter::new(&mut output, line_len, 0, true);
338 encode("filename", &filename, &mut w).unwrap();
339 }
340
341 let output_len = output.len();
343 let mut found_hex_count = 0;
344 for (percent_sign_idx, _) in output.match_indices('%') {
345 assert!(percent_sign_idx + 3 <= output_len);
346
347 let must_be_hex = &output[percent_sign_idx + 1..percent_sign_idx + 3];
349 assert!(
350 must_be_hex == "C3" || must_be_hex == "9C",
351 "unexpected hex char: {}",
352 must_be_hex
353 );
354 found_hex_count += 1;
355 }
356 let number_of_chars_in_hex = 2;
358 assert_eq!(found_hex_count, i * number_of_chars_in_hex);
359
360 let mut last_newline_pos = 0;
362 for (newline_idx, _) in output.match_indices("\r\n") {
363 let line_length = newline_idx - last_newline_pos;
364 assert!(
365 line_length < MAX_LINE_LEN,
366 "expected line length exceeded: {} > {}",
367 line_length,
368 MAX_LINE_LEN
369 );
370 last_newline_pos = newline_idx;
371 }
372 assert_ne!(0, last_newline_pos);
374 }
375 }
376 }
377
378 #[test]
379 #[should_panic(expected = "`key` must only be composed of ascii alphanumeric chars")]
380 fn non_ascii_key() {
381 let mut s = String::new();
382 let mut w = EmailWriter::new(&mut s, 0, 0, true);
383 let _ = encode("π¬", "", &mut w);
384 }
385}