Skip to main content

mail_parser/parsers/
header.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 super::MessageStream;
8use crate::{Header, HeaderName, MessageParser, header_map};
9use std::borrow::Cow;
10const MAX_HEADER_NAME_LEN: usize = 34;
11
12impl<'x> MessageStream<'x> {
13    pub fn parse_headers(&mut self, conf: &MessageParser, headers: &mut Vec<Header<'x>>) -> bool {
14        loop {
15            loop {
16                match self.peek() {
17                    Some(b'\n') => {
18                        self.next();
19                        return true;
20                    }
21                    None => return false,
22                    Some(ch) if !ch.is_ascii_whitespace() => {
23                        break;
24                    }
25                    _ => {
26                        self.next();
27                    }
28                }
29            }
30
31            let offset_field = self.offset();
32
33            if let Some(header_name) = self.parse_header_name() {
34                let from_offset = self.offset();
35                let value = if conf.header_map.is_empty() {
36                    match &header_name {
37                        HeaderName::Subject
38                        | HeaderName::Comments
39                        | HeaderName::ContentDescription
40                        | HeaderName::ContentLocation
41                        | HeaderName::ContentTransferEncoding => self.parse_unstructured(),
42                        HeaderName::From
43                        | HeaderName::To
44                        | HeaderName::Cc
45                        | HeaderName::Bcc
46                        | HeaderName::ReplyTo
47                        | HeaderName::Sender
48                        | HeaderName::ResentTo
49                        | HeaderName::ResentFrom
50                        | HeaderName::ResentBcc
51                        | HeaderName::ResentCc
52                        | HeaderName::ResentSender
53                        | HeaderName::ListArchive
54                        | HeaderName::ListHelp
55                        | HeaderName::ListId
56                        | HeaderName::ListOwner
57                        | HeaderName::ListPost
58                        | HeaderName::ListSubscribe
59                        | HeaderName::ListUnsubscribe => self.parse_address(),
60                        HeaderName::Date | HeaderName::ResentDate => self.parse_date(),
61                        HeaderName::MessageId
62                        | HeaderName::References
63                        | HeaderName::InReplyTo
64                        | HeaderName::ReturnPath
65                        | HeaderName::ContentId
66                        | HeaderName::ResentMessageId => self.parse_id(),
67                        HeaderName::Keywords | HeaderName::ContentLanguage => {
68                            self.parse_comma_separared()
69                        }
70                        HeaderName::Received => self.parse_received(),
71                        HeaderName::MimeVersion => self.parse_raw(),
72                        HeaderName::ContentType | HeaderName::ContentDisposition => {
73                            self.parse_content_type()
74                        }
75                        _ => self.parse_raw(),
76                    }
77                } else {
78                    (conf
79                        .header_map
80                        .get(&header_name)
81                        .unwrap_or(&conf.def_hdr_parse_fnc))(self)
82                };
83
84                headers.push(Header {
85                    name: header_name,
86                    value,
87                    offset_field: offset_field as u32,
88                    offset_start: from_offset as u32,
89                    offset_end: self.offset() as u32,
90                });
91            } else if self.is_eof() {
92                return false;
93            }
94        }
95    }
96
97    pub fn parse_header_name(&mut self) -> Option<HeaderName<'x>> {
98        let mut token_start: usize = 0;
99        let mut token_end: usize = 0;
100        let mut token_len: usize = 0;
101
102        let mut header = [0u8; MAX_HEADER_NAME_LEN];
103
104        while let Some(&ch) = self.next() {
105            match ch {
106                b':' => {
107                    if token_start != 0 {
108                        break;
109                    }
110                }
111                b'\n' => {
112                    return None;
113                }
114                _ => {
115                    if !ch.is_ascii_whitespace() {
116                        if token_start == 0 {
117                            token_start = self.offset();
118                            token_end = token_start;
119                        } else {
120                            token_end = self.offset();
121                        }
122
123                        if let Some(header) = header.get_mut(token_len) {
124                            *header = ch.to_ascii_lowercase();
125                        }
126                        token_len += 1;
127                    }
128                }
129            }
130        }
131
132        if token_start != 0 {
133            header
134                .get(..token_len)
135                .and_then(header_map)
136                .unwrap_or_else(|| {
137                    HeaderName::Other(String::from_utf8_lossy(
138                        self.bytes(token_start - 1..token_end),
139                    ))
140                })
141                .into()
142        } else {
143            None
144        }
145    }
146}
147
148impl<'x> HeaderName<'x> {
149    /// Parse a header name
150    pub fn parse(data: impl Into<Cow<'x, str>>) -> Option<HeaderName<'x>> {
151        let data = data.into();
152
153        if !data.is_empty() {
154            let mut data_lc = String::with_capacity(data.len());
155            for ch in data.chars() {
156                match ch {
157                    'A'..='Z' => data_lc.push(ch.to_ascii_lowercase()),
158                    'a'..='z' | '0'..='9' | '-' | '_' => data_lc.push(ch),
159                    _ => return None,
160                }
161            }
162            header_map(data_lc.as_bytes())
163                .unwrap_or(HeaderName::Other(data))
164                .into()
165        } else {
166            None
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use crate::{HeaderName, parsers::MessageStream};
174
175    #[test]
176    fn header_name_parse() {
177        let inputs = [
178            ("From: ", HeaderName::From),
179            ("receiVED: ", HeaderName::Received),
180            (" subject   : ", HeaderName::Subject),
181            (
182                "X-Custom-Field : ",
183                HeaderName::Other("X-Custom-Field".into()),
184            ),
185            (" T : ", HeaderName::Other("T".into())),
186            ("mal formed: ", HeaderName::Other("mal formed".into())),
187            ("MIME-version : ", HeaderName::MimeVersion),
188            ("Delivered-To: ", HeaderName::DeliveredTo),
189            ("archived-AT: ", HeaderName::ArchivedAt),
190            ("X-Face: ", HeaderName::Other("X-Face".into())),
191            (
192                "Disposition-Notification-Options: ",
193                HeaderName::DispositionNotificationOptions,
194            ),
195            (
196                "MMHS-Other-Recipients-Indicator-To: ",
197                HeaderName::MmhsOtherRecipientsIndicatorTo,
198            ),
199            (
200                "Original-Encoded-Information-Types-Extra: ",
201                HeaderName::Other("Original-Encoded-Information-Types-Extra".into()),
202            ),
203        ];
204
205        for (input, expected_result) in inputs {
206            assert_eq!(
207                expected_result,
208                MessageStream::new(input.as_bytes())
209                    .parse_header_name()
210                    .unwrap(),
211                "Failed to parse '{input:?}'",
212            );
213        }
214    }
215}