1use bounded_static::ToStatic;
2#[cfg(feature = "tracing")]
3use tracing::warn;
4
5#[cfg(feature = "arbitrary")]
6use crate::fuzz_eq::FuzzEq;
7use crate::header;
8use crate::imf::address::{nullable_address_list, AddressList};
9use crate::imf::datetime::{date_time, DateTime};
10use crate::imf::identification::{msg_id, nullable_msg_list, MessageID, MessageIDList};
11use crate::imf::mailbox::{mailbox, mailbox_list, MailboxList, MailboxRef};
12use crate::imf::mime::{version, Version};
13use crate::imf::trace::{return_path, ReturnPath};
14use crate::print::{Formatter, Print};
15use crate::text::misc_token::{phrase_list, unstructured, PhraseList, Unstructured};
16#[cfg(feature = "tracing-unsupported")]
17use crate::utils::bytes_to_trace_string;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, ToStatic)]
20#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
21pub enum Entry {
22 Date,
23 From,
24 Sender,
25 ReplyTo,
26 To,
27 Cc,
28 Bcc,
29 MessageID,
30 InReplyTo,
31 References,
32 Subject,
33 #[cfg_attr(feature = "arbitrary", fuzz_eq(use_eq))]
34 Comments(usize),
35 #[cfg_attr(feature = "arbitrary", fuzz_eq(use_eq))]
36 Keywords(usize),
37 #[cfg_attr(feature = "arbitrary", fuzz_eq(use_eq))]
38 Trace(usize), MIMEVersion,
40}
41
42#[derive(Clone, Debug, PartialEq, ToStatic)]
43#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
44pub enum Field<'a> {
45 Date(DateTime),
47
48 From(MailboxList<'a>),
50 Sender(MailboxRef<'a>),
51 ReplyTo(AddressList<'a>),
52
53 To(AddressList<'a>),
55 Cc(AddressList<'a>),
56 Bcc(AddressList<'a>),
57
58 MessageID(MessageID<'a>),
60 InReplyTo(MessageIDList<'a>),
61 References(MessageIDList<'a>),
62
63 Subject(Unstructured<'a>),
65 Comments(Unstructured<'a>),
66 Keywords(PhraseList<'a>),
67
68 Received(Unstructured<'a>),
71 ReturnPath(ReturnPath<'a>),
72
73 MIMEVersion(Version),
75}
76
77impl<'a> Field<'a> {
78 pub fn raw_name(&self) -> header::FieldName<'static> {
79 match self {
80 Self::Date(_) => header::FieldName(b"Date".into()),
81 Self::From(_) => header::FieldName(b"From".into()),
82 Self::Sender(_) => header::FieldName(b"Sender".into()),
83 Self::ReplyTo(_) => header::FieldName(b"Reply-To".into()),
84 Self::To(_) => header::FieldName(b"To".into()),
85 Self::Cc(_) => header::FieldName(b"Cc".into()),
86 Self::Bcc(_) => header::FieldName(b"Bcc".into()),
87 Self::MessageID(_) => header::FieldName(b"Message-Id".into()),
88 Self::InReplyTo(_) => header::FieldName(b"In-Reply-To".into()),
89 Self::References(_) => header::FieldName(b"References".into()),
90 Self::Subject(_) => header::FieldName(b"Subject".into()),
91 Self::Comments(_) => header::FieldName(b"Comments".into()),
92 Self::Keywords(_) => header::FieldName(b"Keywords".into()),
93 Self::Received(_) => header::FieldName(b"Received".into()),
94 Self::ReturnPath(_) => header::FieldName(b"Return-Path".into()),
95 Self::MIMEVersion(_) => header::FieldName(b"MIME-Version".into()),
96 }
97 }
98}
99impl<'a> Print for Field<'a> {
100 fn print(&self, fmt: &mut impl Formatter) {
101 match self {
102 Self::Date(d) => header::print(fmt, b"Date", d),
103 Self::From(mboxl) => header::print(fmt, b"From", mboxl),
104 Self::Sender(mbox) => header::print(fmt, b"Sender", mbox),
105 Self::ReplyTo(addrs) => header::print(fmt, b"Reply-To", addrs),
106 Self::To(addrs) => header::print(fmt, b"To", addrs),
107 Self::Cc(addrs) => header::print(fmt, b"Cc", addrs),
108 Self::Bcc(addrs) => header::print(fmt, b"Bcc", addrs),
109 Self::MessageID(id) => header::print(fmt, b"Message-ID", id),
110 Self::InReplyTo(ids) => header::print(fmt, b"In-Reply-To", ids),
111 Self::References(ids) => header::print(fmt, b"References", ids),
112 Self::Subject(u) => header::print_unstructured(fmt, b"Subject", u),
113 Self::Comments(u) => header::print_unstructured(fmt, b"Comments", u),
114 Self::Keywords(l) => header::print(fmt, b"Keywords", l),
115 Self::Received(u) => header::print_unstructured(fmt, b"Received", u),
116 Self::ReturnPath(p) => header::print(fmt, b"Return-Path", p),
117 Self::MIMEVersion(v) => header::print(fmt, b"MIME-Version", v),
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy)]
123pub enum InvalidField {
124 Name,
126 Body,
128 NeedsDiscard,
132}
133
134impl<'a> TryFrom<&header::FieldRaw<'a>> for Field<'a> {
135 type Error = InvalidField;
136
137 #[cfg_attr(
138 feature = "tracing",
139 tracing::instrument(name = "imf::field::Field::try_from")
140 )]
141 fn try_from(f: &header::FieldRaw<'a>) -> Result<Self, Self::Error> {
142 fn bind_res<T, U, F>(res: nom::IResult<&[u8], T>, f: F) -> Result<U, InvalidField>
143 where
144 F: Fn(T) -> Result<U, InvalidField>,
145 {
146 match res {
147 Ok((b"", content)) => f(content),
148 Ok((_rest, _)) => {
149 #[cfg(feature = "tracing-unsupported")]
151 warn!(rest = %bytes_to_trace_string(_rest),
152 "leftover input after parsing");
153 Err(InvalidField::Body)
154 }
155 Err(_) => Err(InvalidField::Body),
156 }
157 }
158 fn map_res<T, U, F>(res: nom::IResult<&[u8], T>, f: F) -> Result<U, InvalidField>
159 where
160 F: Fn(T) -> U,
161 {
162 bind_res(res, |x| Ok(f(x)))
163 }
164
165 match f.name.bytes().to_ascii_lowercase().as_slice() {
166 b"date" => map_res(date_time(f.body), Field::Date),
167 b"from" => map_res(mailbox_list(f.body), Field::From),
168 b"sender" => map_res(mailbox(f.body), Field::Sender),
169 b"reply-to" => bind_res(nullable_address_list(f.body), |addrs| {
170 if addrs.is_empty() {
171 Err(InvalidField::NeedsDiscard)
172 } else {
173 Ok(Field::ReplyTo(addrs))
174 }
175 }),
176 b"to" => bind_res(nullable_address_list(f.body), |addrs| {
177 if addrs.is_empty() {
178 Err(InvalidField::NeedsDiscard)
179 } else {
180 Ok(Field::To(addrs))
181 }
182 }),
183 b"cc" => bind_res(nullable_address_list(f.body), |addrs| {
184 if addrs.is_empty() {
185 Err(InvalidField::NeedsDiscard)
186 } else {
187 Ok(Field::Cc(addrs))
188 }
189 }),
190 b"bcc" => map_res(nullable_address_list(f.body), Field::Bcc),
191 b"message-id" => map_res(msg_id(f.body), Field::MessageID),
192 b"in-reply-to" => bind_res(nullable_msg_list(f.body), |msgl| {
193 if msgl.is_empty() {
196 Err(InvalidField::NeedsDiscard)
197 } else {
198 Ok(Field::InReplyTo(msgl))
199 }
200 }),
201 b"references" => bind_res(nullable_msg_list(f.body), |msgl| {
202 if msgl.is_empty() {
205 Err(InvalidField::NeedsDiscard)
206 } else {
207 Ok(Field::References(msgl))
208 }
209 }),
210 b"subject" => map_res(unstructured(f.body), Field::Subject),
211 b"comments" => map_res(unstructured(f.body), Field::Comments),
212 b"keywords" => bind_res(phrase_list(f.body), |opt| {
213 match opt {
216 None => Err(InvalidField::NeedsDiscard),
217 Some(kwds) => Ok(Field::Keywords(kwds)),
218 }
219 }),
220 b"return-path" => map_res(return_path(f.body), Field::ReturnPath),
221 b"received" => map_res(unstructured(f.body), Field::Received),
222 b"mime-version" => map_res(version(f.body), Field::MIMEVersion),
223 _ => Err(InvalidField::Name),
224 }
225 }
226}
227
228impl<'a> TryFrom<&header::Unstructured<'a>> for Field<'static> {
229 type Error = InvalidField;
230
231 fn try_from(u: &header::Unstructured<'a>) -> Result<Self, Self::Error> {
232 use bounded_static::IntoBoundedStatic;
233 use std::borrow::Cow;
234 let bytes_body: Cow<[u8]> = match u.raw_body.0 {
235 Some(s) => s.into(),
236 None => u.body.to_string_keep_obs().into_bytes().into(),
237 };
238 let hdr = header::FieldRaw {
239 name: u.name.clone(),
240 body: &bytes_body,
241 };
242 Field::try_from(&hdr).map(IntoBoundedStatic::into_static)
243 }
244}
245
246pub fn is_imf_header(name: &header::FieldName) -> bool {
247 matches!(
248 name.bytes().to_ascii_lowercase().as_slice(),
249 b"date"
250 | b"from"
251 | b"sender"
252 | b"reply-to"
253 | b"to"
254 | b"cc"
255 | b"bcc"
256 | b"message-id"
257 | b"in-reply-to"
258 | b"references"
259 | b"subject"
260 | b"comments"
261 | b"keywords"
262 | b"return-path"
263 | b"received"
264 | b"mime-version"
265 )
266}