1use crate::headermap::EncodeHeaderValue;
2use crate::rfc5322_parser::{qp_encode, Parser};
3use crate::{
4 AddressList, AuthenticationResults, MailParsingError, Mailbox, MailboxList, MessageID,
5 MimeParameters, Result, SharedString,
6};
7use bstr::BString;
8use chrono::{DateTime, FixedOffset};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum ParsedHeader {
16 MailboxList(MailboxList),
17 Mailbox(Mailbox),
18 AddressList(AddressList),
19 MessageId(MessageID),
20 MessageIdList(Vec<MessageID>),
21 MimeParameters(MimeParameters),
22 Date(DateTime<FixedOffset>),
23 AuthenticationResults(AuthenticationResults),
24 Unstructured(BString),
25}
26
27impl ParsedHeader {
28 pub fn structured(name: &[u8], value: &[u8]) -> Result<Self> {
31 Ok(match grammar_for_name(name) {
32 Grammar::MailboxList => Self::MailboxList(Parser::parse_mailbox_list_header(value)?),
33 Grammar::Mailbox => Self::Mailbox(Parser::parse_mailbox_header(value)?),
34 Grammar::AddressList => Self::AddressList(Parser::parse_address_list_header(value)?),
35 Grammar::MessageId => Self::MessageId(Parser::parse_msg_id_header(value)?),
36 Grammar::ContentId => Self::MessageId(Parser::parse_content_id_header(value)?),
37 Grammar::MessageIdList => Self::MessageIdList(Parser::parse_msg_id_header_list(value)?),
38 Grammar::ContentType => Self::MimeParameters(Parser::parse_content_type_header(value)?),
39 Grammar::ContentTransferEncoding | Grammar::ContentDisposition => {
43 Self::MimeParameters(Parser::parse_content_transfer_encoding_header(value)?)
44 }
45 Grammar::Date => {
46 let value = std::str::from_utf8(value).map_err(|_| MailParsingError::EightBit)?;
47 Self::Date(crate::parse_rfc2822_date(value).map_err(MailParsingError::ChronoError)?)
48 }
49 Grammar::AuthenticationResults => {
50 Self::AuthenticationResults(Parser::parse_authentication_results_header(value)?)
51 }
52 Grammar::Unstructured => Self::Unstructured(Parser::parse_unstructured_header(value)?),
53 })
54 }
55}
56
57impl EncodeHeaderValue for ParsedHeader {
58 fn encode_value(&self) -> SharedString<'static> {
59 match self {
60 Self::MailboxList(v) => v.encode_value(),
61 Self::Mailbox(v) => v.encode_value(),
62 Self::AddressList(v) => v.encode_value(),
63 Self::MessageId(v) => v.encode_value(),
64 Self::MessageIdList(v) => v.encode_value(),
65 Self::MimeParameters(v) => v.encode_value(),
66 Self::Date(v) => v.encode_value(),
67 Self::AuthenticationResults(v) => v.encode_value(),
68 Self::Unstructured(v) => encode_unstructured_value(v.as_slice()),
69 }
70 }
71}
72
73pub(crate) fn encode_unstructured_value(value: &[u8]) -> SharedString<'static> {
78 match std::str::from_utf8(value) {
79 Ok(value) if value.is_ascii() => kumo_wrap::wrap_bytes(value).into(),
80 Ok(value) => qp_encode(value.as_bytes()).into(),
81 Err(_) => kumo_wrap::wrap_bytes(value).into(),
82 }
83}
84
85#[derive(Clone, Copy, PartialEq, Eq)]
89enum Grammar {
90 MailboxList,
91 Mailbox,
92 AddressList,
93 MessageId,
94 ContentId,
95 MessageIdList,
96 ContentType,
97 ContentTransferEncoding,
98 ContentDisposition,
99 Date,
100 AuthenticationResults,
101 Unstructured,
102}
103
104const KNOWN_HEADERS: &[(&str, Grammar)] = &[
110 ("From", Grammar::MailboxList),
111 ("Resent-From", Grammar::MailboxList),
112 ("Sender", Grammar::Mailbox),
113 ("Resent-Sender", Grammar::Mailbox),
114 ("Reply-To", Grammar::AddressList),
115 ("To", Grammar::AddressList),
116 ("Cc", Grammar::AddressList),
117 ("Bcc", Grammar::AddressList),
118 ("Resent-To", Grammar::AddressList),
119 ("Resent-Cc", Grammar::AddressList),
120 ("Resent-Bcc", Grammar::AddressList),
121 ("Date", Grammar::Date),
122 ("Message-ID", Grammar::MessageId),
123 ("Content-ID", Grammar::ContentId),
124 ("References", Grammar::MessageIdList),
125 ("Content-Type", Grammar::ContentType),
126 (
127 "Content-Transfer-Encoding",
128 Grammar::ContentTransferEncoding,
129 ),
130 ("Content-Disposition", Grammar::ContentDisposition),
131 ("Authentication-Results", Grammar::AuthenticationResults),
132 ("Subject", Grammar::Unstructured),
133 ("Comments", Grammar::Unstructured),
134 ("MIME-Version", Grammar::Unstructured),
135];
136
137fn grammar_for_name(name: &[u8]) -> Grammar {
138 KNOWN_HEADERS
139 .iter()
140 .find(|(known, _)| known.as_bytes().eq_ignore_ascii_case(name))
141 .map(|(_, grammar)| *grammar)
142 .unwrap_or(Grammar::Unstructured)
143}
144
145pub fn canonical_header_name(name: &[u8]) -> Option<&'static str> {
147 KNOWN_HEADERS
148 .iter()
149 .find(|(known, _)| known.as_bytes().eq_ignore_ascii_case(name))
150 .map(|(known, _)| *known)
151}
152
153pub fn is_address_header_name(name: &[u8]) -> bool {
156 matches!(
157 grammar_for_name(name),
158 Grammar::MailboxList | Grammar::Mailbox | Grammar::AddressList
159 )
160}
161
162#[cfg(test)]
163mod test {
164 use super::*;
165
166 #[test]
167 fn address_header_names() {
168 assert!(is_address_header_name(b"from"));
171 assert!(is_address_header_name(b"To"));
172 assert!(is_address_header_name(b"BCC"));
173 assert!(is_address_header_name(b"Sender"));
174 assert!(!is_address_header_name(b"Subject"));
175 assert!(!is_address_header_name(b"Date"));
176 assert!(!is_address_header_name(b"X-Custom"));
177 }
178
179 #[test]
180 fn canonical_names() {
181 k9::assert_equal!(canonical_header_name(b"message-id"), Some("Message-ID"));
182 k9::assert_equal!(canonical_header_name(b"REPLY-TO"), Some("Reply-To"));
183 k9::assert_equal!(canonical_header_name(b"X-Custom"), None);
184 }
185
186 #[test]
187 fn structured_dispatch() {
188 let to = ParsedHeader::structured(b"To", b"a@example.com, b@example.com").unwrap();
191 match to {
192 ParsedHeader::AddressList(list) => {
193 k9::assert_equal!(list.len(), 2);
194 }
195 other => panic!("expected AddressList, got {other:?}"),
196 }
197
198 let custom = ParsedHeader::structured(b"X-Custom", b"anything at all").unwrap();
199 assert!(matches!(custom, ParsedHeader::Unstructured(_)));
200 }
201}