1use std::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 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 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 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 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 let number_of_chars_in_hex = 2;
356 assert_eq!(found_hex_count, i * number_of_chars_in_hex);
357
358 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 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}