Skip to main content

eml_codec/imf/
field.rs

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), // either a Received or ReturnPath field
39    MIMEVersion,
40}
41
42#[derive(Clone, Debug, PartialEq, ToStatic)]
43#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
44pub enum Field<'a> {
45    // 3.6.1.  The Origination Date Field
46    Date(DateTime),
47
48    // 3.6.2.  Originator Fields
49    From(MailboxList<'a>),
50    Sender(MailboxRef<'a>),
51    ReplyTo(AddressList<'a>),
52
53    // 3.6.3.  Destination Address Fields
54    To(AddressList<'a>),
55    Cc(AddressList<'a>),
56    Bcc(AddressList<'a>),
57
58    // 3.6.4.  Identification Fields
59    MessageID(MessageID<'a>),
60    InReplyTo(MessageIDList<'a>),
61    References(MessageIDList<'a>),
62
63    // 3.6.5.  Informational Fields
64    Subject(Unstructured<'a>),
65    Comments(Unstructured<'a>),
66    Keywords(PhraseList<'a>),
67
68    // 3.6.6   Resent Fields (not implemented)
69    // 3.6.7   Trace Fields
70    Received(Unstructured<'a>),
71    ReturnPath(ReturnPath<'a>),
72
73    // MIME
74    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    /// The field name is not a known IMF field
125    Name,
126    /// The field body could not be parsed
127    Body,
128    /// The field could be parsed but represents a dummy value that is not part
129    /// of the RFC-strict syntax. It must be discarded (no meaningful data is
130    /// lost).
131    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                    // return an error if we haven't parsed the full value
150                    #[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                // the obs syntax allows empty message lists, but not the normal
194                // syntax. we drop them.
195                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                // the obs syntax allows empty message lists, but not the normal
203                // syntax. we drop them.
204                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                // the obs syntax allows empty phrase lists, but not the normal
214                // syntax. we drop them.
215                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}