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