1use std::{
8 iter::{Enumerate, Peekable},
9 slice::Iter,
10};
11
12impl<'x, T> Header<'x, T> {
13 pub fn new(name: &'x [u8], value: &'x [u8], header: T) -> Self {
14 Header {
15 name,
16 value,
17 header,
18 }
19 }
20}
21
22pub trait HeaderStream<'x> {
23 fn next_header(&mut self) -> Option<(&'x [u8], &'x [u8])>;
24 fn body(&mut self) -> &'x [u8];
25}
26
27const MAX_HEADER_LINE_LEN: usize = 76;
28
29pub struct HeaderFolder<'x, W: Writer> {
30 writer: &'x mut W,
31 bytes_left: usize,
32}
33
34impl<'x, W: Writer> HeaderFolder<'x, W> {
35 pub fn new(writer: &'x mut W) -> Self {
36 HeaderFolder {
37 writer,
38 bytes_left: MAX_HEADER_LINE_LEN,
39 }
40 }
41}
42
43impl<'x, W: Writer> Writer for HeaderFolder<'x, W> {
44 fn write(&mut self, buf: &[u8]) {
45 for chunk in buf.split_inclusive(|ch| *ch == b';') {
46 if chunk == b"\r\n" {
47 self.writer.write(chunk);
48 self.bytes_left = MAX_HEADER_LINE_LEN;
49 } else if chunk.len() < self.bytes_left {
50 self.writer.write(chunk);
51 self.bytes_left -= chunk.len();
52 } else if chunk.len() >= MAX_HEADER_LINE_LEN {
53 let mut add_new_line = self.bytes_left != MAX_HEADER_LINE_LEN;
54 let mut last_piece_len = MAX_HEADER_LINE_LEN;
55 for chunk in chunk.chunks(MAX_HEADER_LINE_LEN) {
56 if add_new_line {
57 self.writer.write(b"\r\n\t");
58 }
59 add_new_line = true;
60 self.writer.write(chunk);
61 last_piece_len = chunk.len();
62 }
63 self.bytes_left = MAX_HEADER_LINE_LEN - last_piece_len;
64 } else {
65 self.writer.write(b"\r\n\t");
66 self.writer.write(chunk);
67 self.bytes_left = MAX_HEADER_LINE_LEN - chunk.len();
68 }
69 }
70 }
71}
72
73pub(crate) struct ChainedHeaderIterator<'x, T: Iterator<Item = &'x [u8]>> {
74 parts: T,
75 iter: HeaderIterator<'x>,
76}
77
78pub(crate) struct HeaderIterator<'x> {
79 message: &'x [u8],
80 iter: Peekable<Enumerate<Iter<'x, u8>>>,
81 start_pos: usize,
82}
83
84pub(crate) struct HeaderParser<'x> {
85 message: &'x [u8],
86 iter: Peekable<Enumerate<Iter<'x, u8>>>,
87 start_pos: usize,
88 pub num_received: usize,
89 pub has_message_id: bool,
90 pub has_date: bool,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub(crate) enum AuthenticatedHeader<'x> {
95 Ds(&'x [u8]),
96 D2s(&'x [u8]),
97 D2i(&'x [u8]),
98 #[cfg(feature = "arc")]
99 Aar(&'x [u8]),
100 #[cfg(feature = "arc")]
101 Ams(&'x [u8]),
102 #[cfg(feature = "arc")]
103 As(&'x [u8]),
104 From(&'x [u8]),
105 Other(&'x [u8]),
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct Header<'x, T> {
110 pub name: &'x [u8],
111 pub value: &'x [u8],
112 pub header: T,
113}
114
115impl<'x> HeaderParser<'x> {
116 pub fn new(message: &'x [u8]) -> Self {
117 HeaderParser {
118 message,
119 iter: message.iter().enumerate().peekable(),
120 start_pos: 0,
121 num_received: 0,
122 has_message_id: false,
123 has_date: false,
124 }
125 }
126
127 pub fn body_offset(&mut self) -> Option<usize> {
128 self.iter.peek().map(|(pos, _)| *pos)
129 }
130}
131
132impl<'x> HeaderIterator<'x> {
133 pub fn new(message: &'x [u8]) -> Self {
134 HeaderIterator {
135 message,
136 iter: message.iter().enumerate().peekable(),
137 start_pos: 0,
138 }
139 }
140
141 pub fn seek_start(&mut self) {
142 while let Some((_, ch)) = self.iter.peek() {
143 if !ch.is_ascii_whitespace() {
144 break;
145 } else {
146 self.iter.next();
147 }
148 }
149 }
150
151 pub fn body_offset(&mut self) -> Option<usize> {
152 self.iter.peek().map(|(pos, _)| *pos)
153 }
154}
155
156impl<'x> HeaderStream<'x> for HeaderIterator<'x> {
157 fn next_header(&mut self) -> Option<(&'x [u8], &'x [u8])> {
158 self.next()
159 }
160
161 fn body(&mut self) -> &'x [u8] {
162 self.body_offset()
163 .and_then(|offset| self.message.get(offset..))
164 .unwrap_or_default()
165 }
166}
167
168impl<'x> Iterator for HeaderIterator<'x> {
169 type Item = (&'x [u8], &'x [u8]);
170
171 fn next(&mut self) -> Option<Self::Item> {
172 let mut colon_pos = usize::MAX;
173 let mut last_ch = 0;
174
175 while let Some((pos, &ch)) = self.iter.next() {
176 if colon_pos == usize::MAX {
177 match ch {
178 b':' => {
179 colon_pos = pos;
180 }
181 b'\n' => {
182 if last_ch == b'\r' || self.start_pos == pos {
183 return None;
185 } else if self
186 .iter
187 .peek()
188 .is_none_or(|(_, next_byte)| ![b' ', b'\t'].contains(next_byte))
189 {
190 let header_name = self
192 .message
193 .get(self.start_pos..pos + 1)
194 .unwrap_or_default();
195 self.start_pos = pos + 1;
196 return Some((header_name, b""));
197 }
198 }
199 _ => (),
200 }
201 } else if ch == b'\n'
202 && self
203 .iter
204 .peek()
205 .is_none_or(|(_, next_byte)| ![b' ', b'\t'].contains(next_byte))
206 {
207 let header_name = self
208 .message
209 .get(self.start_pos..colon_pos)
210 .unwrap_or_default();
211 let header_value = self.message.get(colon_pos + 1..pos + 1).unwrap_or_default();
212
213 self.start_pos = pos + 1;
214
215 return Some((header_name, header_value));
216 }
217
218 last_ch = ch;
219 }
220
221 None
222 }
223}
224
225impl<'x, T: Iterator<Item = &'x [u8]>> ChainedHeaderIterator<'x, T> {
226 pub fn new(mut parts: T) -> Self {
227 ChainedHeaderIterator {
228 iter: HeaderIterator::new(parts.next().unwrap()),
229 parts,
230 }
231 }
232}
233
234impl<'x, T: Iterator<Item = &'x [u8]>> HeaderStream<'x> for ChainedHeaderIterator<'x, T> {
235 fn next_header(&mut self) -> Option<(&'x [u8], &'x [u8])> {
236 if let Some(header) = self.iter.next_header() {
237 Some(header)
238 } else {
239 self.iter = HeaderIterator::new(self.parts.next()?);
240 self.iter.next_header()
241 }
242 }
243
244 fn body(&mut self) -> &'x [u8] {
245 self.iter.body()
246 }
247}
248
249impl<'x> Iterator for HeaderParser<'x> {
250 type Item = (AuthenticatedHeader<'x>, &'x [u8]);
251
252 fn next(&mut self) -> Option<Self::Item> {
253 let mut colon_pos = usize::MAX;
254 let mut last_ch = 0;
255
256 let mut token_start = usize::MAX;
257 let mut token_end = usize::MAX;
258
259 let mut hash: u64 = 0;
260 let mut hash_shift = 0;
261
262 while let Some((pos, &ch)) = self.iter.next() {
263 if colon_pos == usize::MAX {
264 match ch {
265 b':' => {
266 colon_pos = pos;
267 }
268 b'\n' => {
269 if last_ch == b'\r' || self.start_pos == pos {
270 return None;
272 } else if self
273 .iter
274 .peek()
275 .is_none_or(|(_, next_byte)| ![b' ', b'\t'].contains(next_byte))
276 {
277 let header_name = self
279 .message
280 .get(self.start_pos..pos + 1)
281 .unwrap_or_default();
282 self.start_pos = pos + 1;
283 return Some((AuthenticatedHeader::Other(header_name), b""));
284 }
285 }
286 b' ' | b'\t' | b'\r' => (),
287 b'A'..=b'Z' => {
288 if hash_shift < 64 {
289 hash |= ((ch - b'A' + b'a') as u64) << hash_shift;
290 hash_shift += 8;
291
292 if token_start == usize::MAX {
293 token_start = pos;
294 }
295 }
296 token_end = pos;
297 }
298 b'a'..=b'z' | b'-' | b'0'..=b'9' => {
299 if hash_shift < 64 {
300 hash |= (ch as u64) << hash_shift;
301 hash_shift += 8;
302
303 if token_start == usize::MAX {
304 token_start = pos;
305 }
306 }
307 token_end = pos;
308 }
309 _ => {
310 hash = u64::MAX;
311 }
312 }
313 } else if ch == b'\n'
314 && self
315 .iter
316 .peek()
317 .is_none_or(|(_, next_byte)| ![b' ', b'\t'].contains(next_byte))
318 {
319 let header_name = self
320 .message
321 .get(self.start_pos..colon_pos)
322 .unwrap_or_default();
323 let header_value = self.message.get(colon_pos + 1..pos + 1).unwrap_or_default();
324 let header_name = match hash {
325 RECEIVED if token_start + 8 == token_end + 1 => {
326 self.num_received += 1;
327 AuthenticatedHeader::Other(header_name)
328 }
329 FROM => AuthenticatedHeader::From(header_name),
330 #[cfg(feature = "arc")]
331 AS => AuthenticatedHeader::As(header_name),
332 #[cfg(feature = "arc")]
333 AAR if self
334 .message
335 .get(token_start + 8..token_end + 1)
336 .unwrap_or_default()
337 .eq_ignore_ascii_case(b"entication-Results") =>
338 {
339 AuthenticatedHeader::Aar(header_name)
340 }
341 #[cfg(feature = "arc")]
342 AMS if self
343 .message
344 .get(token_start + 8..token_end + 1)
345 .unwrap_or_default()
346 .eq_ignore_ascii_case(b"age-Signature") =>
347 {
348 AuthenticatedHeader::Ams(header_name)
349 }
350 DKIM if self
351 .message
352 .get(token_start + 8..token_end + 1)
353 .unwrap_or_default()
354 .eq_ignore_ascii_case(b"nature") =>
355 {
356 AuthenticatedHeader::Ds(header_name)
357 }
358 DKIM2
359 if self
360 .message
361 .get(token_start + 8..token_end + 1)
362 .unwrap_or_default()
363 .eq_ignore_ascii_case(b"gnature") =>
364 {
365 AuthenticatedHeader::D2s(header_name)
366 }
367 MSGID
368 if self
369 .message
370 .get(token_start + 8..token_end + 1)
371 .unwrap_or_default()
372 .eq_ignore_ascii_case(b"id") =>
373 {
374 self.has_message_id = true;
375 AuthenticatedHeader::Other(header_name)
376 }
377 MSGID
378 if self
379 .message
380 .get(token_start + 8..token_end + 1)
381 .unwrap_or_default()
382 .eq_ignore_ascii_case(b"instance") =>
383 {
384 AuthenticatedHeader::D2i(header_name)
385 }
386 DATE => {
387 self.has_date = true;
388 AuthenticatedHeader::Other(header_name)
389 }
390 _ => AuthenticatedHeader::Other(header_name),
391 };
392
393 self.start_pos = pos + 1;
394
395 return Some((header_name, header_value));
396 }
397
398 last_ch = ch;
399 }
400
401 None
402 }
403}
404
405pub trait HeaderWriter: Sized {
406 fn write_header(&self, writer: &mut impl Writer);
407 fn to_header(&self) -> String {
408 let mut buf = Vec::new();
409 self.write_header(&mut buf);
410 String::from_utf8(buf).unwrap()
411 }
412}
413
414pub trait Writable {
415 fn write(self, writer: &mut impl Writer);
416}
417
418impl Writable for &[u8] {
419 fn write(self, writer: &mut impl Writer) {
420 writer.write(self);
421 }
422}
423
424pub trait Writer {
425 fn write(&mut self, buf: &[u8]);
426
427 fn write_len(&mut self, buf: &[u8], len: &mut usize) {
428 self.write(buf);
429 *len += buf.len();
430 }
431}
432
433impl Writer for Vec<u8> {
434 fn write(&mut self, buf: &[u8]) {
435 self.extend(buf);
436 }
437}
438
439impl Writer for &mut Vec<u8> {
440 fn write(&mut self, buf: &[u8]) {
441 self.extend(buf);
442 }
443}
444
445const FROM: u64 =
446 (b'f' as u64) | ((b'r' as u64) << 8) | ((b'o' as u64) << 16) | ((b'm' as u64) << 24);
447const DKIM: u64 = (b'd' as u64)
448 | ((b'k' as u64) << 8)
449 | ((b'i' as u64) << 16)
450 | ((b'm' as u64) << 24)
451 | ((b'-' as u64) << 32)
452 | ((b's' as u64) << 40)
453 | ((b'i' as u64) << 48)
454 | ((b'g' as u64) << 56);
455const DKIM2: u64 = (b'd' as u64)
456 | ((b'k' as u64) << 8)
457 | ((b'i' as u64) << 16)
458 | ((b'm' as u64) << 24)
459 | ((b'2' as u64) << 32)
460 | ((b'-' as u64) << 40)
461 | ((b's' as u64) << 48)
462 | ((b'i' as u64) << 56);
463#[cfg(feature = "arc")]
464const AAR: u64 = (b'a' as u64)
465 | ((b'r' as u64) << 8)
466 | ((b'c' as u64) << 16)
467 | ((b'-' as u64) << 24)
468 | ((b'a' as u64) << 32)
469 | ((b'u' as u64) << 40)
470 | ((b't' as u64) << 48)
471 | ((b'h' as u64) << 56);
472#[cfg(feature = "arc")]
473const AMS: u64 = (b'a' as u64)
474 | ((b'r' as u64) << 8)
475 | ((b'c' as u64) << 16)
476 | ((b'-' as u64) << 24)
477 | ((b'm' as u64) << 32)
478 | ((b'e' as u64) << 40)
479 | ((b's' as u64) << 48)
480 | ((b's' as u64) << 56);
481#[cfg(feature = "arc")]
482const AS: u64 = (b'a' as u64)
483 | ((b'r' as u64) << 8)
484 | ((b'c' as u64) << 16)
485 | ((b'-' as u64) << 24)
486 | ((b's' as u64) << 32)
487 | ((b'e' as u64) << 40)
488 | ((b'a' as u64) << 48)
489 | ((b'l' as u64) << 56);
490const RECEIVED: u64 = (b'r' as u64)
491 | ((b'e' as u64) << 8)
492 | ((b'c' as u64) << 16)
493 | ((b'e' as u64) << 24)
494 | ((b'i' as u64) << 32)
495 | ((b'v' as u64) << 40)
496 | ((b'e' as u64) << 48)
497 | ((b'd' as u64) << 56);
498const DATE: u64 =
499 (b'd' as u64) | ((b'a' as u64) << 8) | ((b't' as u64) << 16) | ((b'e' as u64) << 24);
500const MSGID: u64 = (b'm' as u64)
501 | ((b'e' as u64) << 8)
502 | ((b's' as u64) << 16)
503 | ((b's' as u64) << 24)
504 | ((b'a' as u64) << 32)
505 | ((b'g' as u64) << 40)
506 | ((b'e' as u64) << 48)
507 | ((b'-' as u64) << 56);
508
509#[cfg(test)]
510mod test {
511 use super::{ChainedHeaderIterator, HeaderIterator, HeaderStream};
512 use super::{HeaderFolder, MAX_HEADER_LINE_LEN};
513 use crate::common::headers::{AuthenticatedHeader, HeaderParser, Writer};
514
515 #[test]
516 fn header_iterator() {
517 for (message, headers) in [
518 (
519 "From: a\nTo: b\nEmpty:\nMulti: 1\n 2\nSubject: c\n\nNot-header: ignore\n",
520 vec![
521 ("From", " a\n"),
522 ("To", " b\n"),
523 ("Empty", "\n"),
524 ("Multi", " 1\n 2\n"),
525 ("Subject", " c\n"),
526 ],
527 ),
528 (
529 ": a\nTo: b\n \n \nc\n:\nFrom : d\nSubject: e\n\nNot-header: ignore\n",
530 vec![
531 ("", " a\n"),
532 ("To", " b\n \n \n"),
533 ("c\n", ""),
534 ("", "\n"),
535 ("From ", " d\n"),
536 ("Subject", " e\n"),
537 ],
538 ),
539 (
540 concat!(
541 "A: X\r\n",
542 "B : Y\t\r\n",
543 "\tZ \r\n",
544 "\r\n",
545 " C \r\n",
546 "D \t E\r\n"
547 ),
548 vec![("A", " X\r\n"), ("B ", " Y\t\r\n\tZ \r\n")],
549 ),
550 ] {
551 assert_eq!(
552 HeaderIterator::new(message.as_bytes())
553 .map(|(h, v)| {
554 (
555 std::str::from_utf8(h).unwrap(),
556 std::str::from_utf8(v).unwrap(),
557 )
558 })
559 .collect::<Vec<_>>(),
560 headers
561 );
562
563 assert_eq!(
564 HeaderParser::new(message.as_bytes())
565 .map(|(h, v)| {
566 (
567 std::str::from_utf8(match h {
568 #[cfg(feature = "arc")]
569 AuthenticatedHeader::Aar(v)
570 | AuthenticatedHeader::Ams(v)
571 | AuthenticatedHeader::As(v) => v,
572 AuthenticatedHeader::Ds(v)
573 | AuthenticatedHeader::D2s(v)
574 | AuthenticatedHeader::D2i(v)
575 | AuthenticatedHeader::From(v)
576 | AuthenticatedHeader::Other(v) => v,
577 })
578 .unwrap(),
579 std::str::from_utf8(v).unwrap(),
580 )
581 })
582 .collect::<Vec<_>>(),
583 headers
584 );
585 }
586 }
587
588 #[cfg(feature = "arc")]
589 #[test]
590 fn header_parser() {
591 let message = concat!(
592 "ARC-Message-Signature: i=1; a=rsa-sha256;\n",
593 "ARC-Authentication-Results: i=1;\n",
594 "ARC-Seal: i=1; a=rsa-sha256;\n",
595 "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple;\n",
596 "From: jdoe@domain\n",
597 "F r o m : jane@domain.com\n",
598 "ARC-Authentication: i=1;\n",
599 "Received: r1\n",
600 "Received: r2\n",
601 "Received: r3\n",
602 "Received-From: test\n",
603 "Date: date\n",
604 "Message-Id: myid\n",
605 "\nhey",
606 );
607 let mut parser = HeaderParser::new(message.as_bytes());
608 assert_eq!(
609 (&mut parser).map(|(h, _)| { h }).collect::<Vec<_>>(),
610 vec![
611 AuthenticatedHeader::Ams(b"ARC-Message-Signature"),
612 AuthenticatedHeader::Aar(b"ARC-Authentication-Results"),
613 AuthenticatedHeader::As(b"ARC-Seal"),
614 AuthenticatedHeader::Ds(b"DKIM-Signature"),
615 AuthenticatedHeader::From(b"From"),
616 AuthenticatedHeader::From(b"F r o m "),
617 AuthenticatedHeader::Other(b"ARC-Authentication"),
618 AuthenticatedHeader::Other(b"Received"),
619 AuthenticatedHeader::Other(b"Received"),
620 AuthenticatedHeader::Other(b"Received"),
621 AuthenticatedHeader::Other(b"Received-From"),
622 AuthenticatedHeader::Other(b"Date"),
623 AuthenticatedHeader::Other(b"Message-Id"),
624 ]
625 );
626 assert!(parser.has_date);
627 assert!(parser.has_message_id);
628 assert_eq!(parser.num_received, 3);
629 }
630
631 #[test]
632 fn chained_header_iterator() {
633 let parts = [
634 &b"From: a\nTo: b\nEmpty:\nMulti: 1\n 2\n"[..],
635 &b"Subject: c\nReceived: d\n\nhey"[..],
636 ];
637 let mut headers = vec![
638 ("From", " a\n"),
639 ("To", " b\n"),
640 ("Empty", "\n"),
641 ("Multi", " 1\n 2\n"),
642 ("Subject", " c\n"),
643 ("Received", " d\n"),
644 ]
645 .into_iter();
646 let mut it = ChainedHeaderIterator::new(parts.iter().copied());
647
648 while let Some((k, v)) = it.next_header() {
649 assert_eq!(
650 (
651 std::str::from_utf8(k).unwrap(),
652 std::str::from_utf8(v).unwrap()
653 ),
654 headers.next().unwrap()
655 );
656 }
657 assert_eq!(it.body(), b"hey");
658 }
659
660 fn fold(header: &[u8]) -> Vec<u8> {
661 let mut buf = Vec::with_capacity(header.len() + 16);
662 let mut folder = HeaderFolder::new(&mut buf);
663 folder.write(header);
664 buf
665 }
666
667 fn unfold(folded: &[u8]) -> Vec<u8> {
668 let mut out = Vec::with_capacity(folded.len());
669 let mut i = 0;
670 while i < folded.len() {
671 if folded[i..].starts_with(b"\r\n\t") {
672 i += 3;
673 } else {
674 out.push(folded[i]);
675 i += 1;
676 }
677 }
678 out
679 }
680
681 fn assert_folded(original: &[u8]) -> Vec<u8> {
682 let folded = fold(original);
683
684 assert_eq!(
685 unfold(&folded),
686 original,
687 "folding must only insert CRLF+TAB fold points, never alter bytes: {:?}",
688 String::from_utf8_lossy(original)
689 );
690
691 for (n, line) in folded.split(|&c| c == b'\n').enumerate() {
692 let line = line.strip_suffix(b"\r").unwrap_or(line);
693 let content = line.strip_prefix(b"\t").unwrap_or(line);
694 assert!(
695 content.len() <= MAX_HEADER_LINE_LEN,
696 "line {n} of {content_len} bytes exceeds {MAX_HEADER_LINE_LEN}: {:?}",
697 String::from_utf8_lossy(content),
698 content_len = content.len(),
699 );
700 }
701
702 for (i, &ch) in folded.iter().enumerate() {
703 if ch == b'\n' {
704 assert!(
705 i >= 1 && folded[i - 1] == b'\r' && folded.get(i + 1) == Some(&b'\t'),
706 "every LF must be part of a CRLF+TAB fold at offset {i}: {:?}",
707 String::from_utf8_lossy(&folded)
708 );
709 }
710 }
711
712 assert!(
713 !folded.starts_with(b"\r\n\t"),
714 "output must never begin with a fold"
715 );
716
717 folded
718 }
719
720 fn extract_header<'a>(eml: &'a str, name: &str) -> &'a [u8] {
721 eml.lines()
722 .find(|l| {
723 l.len() > name.len()
724 && l.as_bytes()[..name.len()].eq_ignore_ascii_case(name.as_bytes())
725 && l.as_bytes()[name.len()] == b':'
726 })
727 .unwrap_or_else(|| panic!("header {name} not found"))
728 .as_bytes()
729 }
730
731 #[test]
732 fn header_folder_passthrough() {
733 for input in [
734 &b""[..],
735 &b";"[..],
736 &b"a;;b;;;c"[..],
737 &b"Subject: hello world"[..],
738 &b"Dkim2-Signature: i=1; m=1; d=test.dkim2.eu"[..],
739 ] {
740 assert_eq!(
741 fold(input),
742 input,
743 "should pass through unchanged: {:?}",
744 String::from_utf8_lossy(input)
745 );
746 }
747
748 let just_under = vec![b'a'; MAX_HEADER_LINE_LEN - 1];
749 assert_eq!(fold(&just_under), just_under);
750 }
751
752 #[test]
753 fn header_folder_boundaries() {
754 let exactly_max = vec![b'a'; MAX_HEADER_LINE_LEN];
755 let folded = assert_folded(&exactly_max);
756 assert_eq!(folded, exactly_max, "76 bytes fit on one line, no fold");
757
758 let over_max = vec![b'a'; MAX_HEADER_LINE_LEN + 1];
759 let folded = assert_folded(&over_max);
760 let mut expected = vec![b'a'; MAX_HEADER_LINE_LEN];
761 expected.extend_from_slice(b"\r\n\t");
762 expected.push(b'a');
763 assert_eq!(folded, expected, "77 bytes wrap into 76 + fold + 1");
764
765 let two_pieces = vec![b'a'; MAX_HEADER_LINE_LEN * 2];
766 let folded = assert_folded(&two_pieces);
767 assert_eq!(
768 folded.iter().filter(|&&c| c == b'\n').count(),
769 1,
770 "an exact multiple of the limit yields exactly one fold"
771 );
772 }
773
774 #[test]
775 fn header_folder_large_chunk_followed_by_tags() {
776 let big = vec![b'A'; MAX_HEADER_LINE_LEN * 2 - 3];
777 let mut input = b"v=".to_vec();
778 input.extend_from_slice(&big);
779 input.extend_from_slice(b";a=1;b=2;c=3;d=4;e=5");
780 assert_folded(&input);
781
782 let big = vec![b'A'; MAX_HEADER_LINE_LEN + 20];
783 let mut input = b"s=".to_vec();
784 input.extend_from_slice(&big);
785 input.extend_from_slice(b";f=feedback");
786 assert_folded(&input);
787 }
788
789 #[test]
790 fn header_folder_consecutive_large_chunks() {
791 let mf = vec![b'A'; 100];
792 let rt = vec![b'B'; 90];
793 let mut input = b"Dkim2-Signature:mf=".to_vec();
794 input.extend_from_slice(&mf);
795 input.extend_from_slice(b";rt=");
796 input.extend_from_slice(&rt);
797 input.extend_from_slice(b";f=feedback");
798 assert_folded(&input);
799 }
800
801 #[test]
802 fn header_folder_many_small_tags() {
803 let mut input = b"Dkim2-Signature:".to_vec();
804 for i in 0..40 {
805 input.extend_from_slice(format!(" tag{i}=value{i};").as_bytes());
806 }
807 assert_folded(&input);
808 }
809
810 #[test]
811 fn header_folder_large_leading_chunk_over_partial_line() {
812 let mut input = b"Message-Instance: m=1; h=sha256:".to_vec();
813 input.extend_from_slice(&[b'Z'; 120]);
814 assert_folded(&input);
815 }
816
817 #[test]
818 fn header_folder_real_dkim2_headers() {
819 const FILES: [&str; 2] = [
820 include_str!("../../resources/dkim2/expected/d2_duplicate_rt_tag.eml"),
821 include_str!("../../resources/dkim2/expected/pkix_rsa8192.eml"),
822 ];
823
824 for eml in FILES {
825 for name in ["Message-Instance", "Dkim2-Signature"] {
826 let header = extract_header(eml, name);
827 assert!(
828 header.len() > MAX_HEADER_LINE_LEN,
829 "{name} should exceed the fold limit to exercise folding"
830 );
831 let folded = assert_folded(header);
832 assert!(
833 folded.windows(3).any(|w| w == b"\r\n\t"),
834 "long real header {name} should have been folded"
835 );
836 }
837 }
838 }
839}