1pub 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 pub date: Option<DateTime<FixedOffset>>,
26
27 pub from: Vec<MailboxRef<'a>>,
29 pub sender: Option<MailboxRef<'a>>,
30 pub reply_to: Vec<AddressRef<'a>>,
31
32 pub to: Vec<AddressRef<'a>>,
34 pub cc: Vec<AddressRef<'a>>,
35 pub bcc: Vec<AddressRef<'a>>,
36
37 pub msg_id: Option<MessageID<'a>>,
39 pub in_reply_to: Vec<MessageID<'a>>,
40 pub references: Vec<MessageID<'a>>,
41
42 pub subject: Option<Unstructured<'a>>,
44 pub comments: Vec<Unstructured<'a>>,
45 pub keywords: Vec<PhraseList<'a>>,
46
47 pub return_path: Vec<AddrSpec<'a>>,
50 pub received: Vec<ReceivedLog<'a>>,
51
52 pub mime_version: Option<Version>,
54}
55
56impl<'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}