Skip to main content

mail_auth/common/
headers.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use 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                            // End of headers
184                            return None;
185                        } else if self
186                            .iter
187                            .peek()
188                            .is_none_or(|(_, next_byte)| ![b' ', b'\t'].contains(next_byte))
189                        {
190                            // Invalid header, return anyway.
191                            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                            // End of headers
271                            return None;
272                        } else if self
273                            .iter
274                            .peek()
275                            .is_none_or(|(_, next_byte)| ![b' ', b'\t'].contains(next_byte))
276                        {
277                            // Invalid header, return anyway.
278                            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}