eml_codec/imf/
mod.rs

1/// Parse and represent IMF (Internet Message Format) headers (RFC822, RFC5322)
2pub mod address;
3pub mod datetime;
4pub mod field;
5pub mod identification;
6pub mod mailbox;
7pub mod mime;
8pub mod trace;
9
10use nom::{combinator::map, IResult};
11
12use crate::header;
13use crate::imf::address::AddressRef;
14use crate::imf::field::Field;
15use crate::imf::identification::MessageID;
16use crate::imf::mailbox::{AddrSpec, MailboxRef};
17use crate::imf::mime::Version;
18use crate::imf::trace::ReceivedLog;
19use crate::text::misc_token::{PhraseList, Unstructured};
20use chrono::{DateTime, FixedOffset};
21
22#[derive(Debug, PartialEq, Default)]
23pub struct Imf<'a> {
24    // 3.6.1.  The Origination Date Field
25    pub date: Option<DateTime<FixedOffset>>,
26
27    // 3.6.2.  Originator Fields
28    pub from: Vec<MailboxRef<'a>>,
29    pub sender: Option<MailboxRef<'a>>,
30    pub reply_to: Vec<AddressRef<'a>>,
31
32    // 3.6.3.  Destination Address Fields
33    pub to: Vec<AddressRef<'a>>,
34    pub cc: Vec<AddressRef<'a>>,
35    pub bcc: Vec<AddressRef<'a>>,
36
37    // 3.6.4.  Identification Fields
38    pub msg_id: Option<MessageID<'a>>,
39    pub in_reply_to: Vec<MessageID<'a>>,
40    pub references: Vec<MessageID<'a>>,
41
42    // 3.6.5.  Informational Fields
43    pub subject: Option<Unstructured<'a>>,
44    pub comments: Vec<Unstructured<'a>>,
45    pub keywords: Vec<PhraseList<'a>>,
46
47    // 3.6.6 Not implemented
48    // 3.6.7 Trace Fields
49    pub return_path: Vec<AddrSpec<'a>>,
50    pub received: Vec<ReceivedLog<'a>>,
51
52    // MIME
53    pub mime_version: Option<Version>,
54}
55
56//@FIXME min and max limits are not enforced,
57// it may result in missing data or silently overriden data.
58impl<'a> FromIterator<Field<'a>> for Imf<'a> {
59    fn from_iter<I: IntoIterator<Item = Field<'a>>>(iter: I) -> Self {
60        iter.into_iter().fold(Imf::default(), |mut section, field| {
61            match field {
62                Field::Date(v) => section.date = v,
63                Field::From(v) => section.from.extend(v),
64                Field::Sender(v) => section.sender = Some(v),
65                Field::ReplyTo(v) => section.reply_to.extend(v),
66                Field::To(v) => section.to.extend(v),
67                Field::Cc(v) => section.cc.extend(v),
68                Field::Bcc(v) => section.bcc.extend(v),
69                Field::MessageID(v) => section.msg_id = Some(v),
70                Field::InReplyTo(v) => section.in_reply_to.extend(v),
71                Field::References(v) => section.references.extend(v),
72                Field::Subject(v) => section.subject = Some(v),
73                Field::Comments(v) => section.comments.push(v),
74                Field::Keywords(v) => section.keywords.push(v),
75                Field::ReturnPath(v) => v.map(|x| section.return_path.push(x)).unwrap_or(()),
76                Field::Received(v) => section.received.push(v),
77                Field::MIMEVersion(v) => section.mime_version = Some(v),
78            };
79            section
80        })
81    }
82}
83
84pub fn imf(input: &[u8]) -> IResult<&[u8], Imf> {
85    map(header::header_kv, |fields| {
86        fields
87            .iter()
88            .flat_map(Field::try_from)
89            .into_iter()
90            .collect::<Imf>()
91    })(input)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::imf::address::*;
98    use crate::imf::mailbox::*;
99    use crate::text::misc_token::*;
100    use chrono::{FixedOffset, TimeZone};
101
102    #[test]
103    fn test_header() {
104        let fullmail = b"Date: 7 Mar 2023 08:00:00 +0200
105From: someone@example.com
106To: someone_else@example.com
107Subject: An RFC 822 formatted message
108
109This is the plain text body of the message. Note the blank line
110between the header information and the body of the message.";
111
112        assert_eq!(
113            imf(fullmail),
114            Ok((
115                &b"This is the plain text body of the message. Note the blank line\nbetween the header information and the body of the message."[..],
116                Imf {
117                    date: Some(FixedOffset::east_opt(2 * 3600).unwrap().with_ymd_and_hms(2023, 3, 7, 8, 0, 0).unwrap()),
118                    from: vec![MailboxRef {
119                        name: None,
120                        addrspec: AddrSpec {
121                            local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"someone"[..]))]),
122                            domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
123                        }
124                    }],
125                    to: vec![AddressRef::Single(MailboxRef {
126                        name: None,
127                        addrspec: AddrSpec {
128                            local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"someone_else"[..]))]),
129                            domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
130                        }
131                    })],
132                    subject: Some(Unstructured(vec![
133                        UnstrToken::Plain(&b"An"[..]),
134                        UnstrToken::Plain(&b"RFC"[..]),
135                        UnstrToken::Plain(&b"822"[..]), 
136                        UnstrToken::Plain(&b"formatted"[..]), 
137                        UnstrToken::Plain(&b"message"[..]),
138                    ])),
139                    ..Imf::default()
140                }
141            )),
142        )
143    }
144}