email_parser/
email.rs

1use crate::address::*;
2use crate::prelude::*;
3use std::borrow::Cow;
4
5/// A struct representing a valid RFC 5322 message.
6///
7/// # Example
8///
9/// ```
10/// # use email_parser::prelude::*;
11/// let email = Email::parse(
12///     b"\
13///     From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
14///     Subject:Example Email\r\n\
15///     To: Someone <example@example.com>\r\n\
16///     Message-id: <6546518945@mubelotix.dev>\r\n\
17///     Date: 5 May 2003 18:58:34 +0000\r\n\
18///     \r\n\
19///     Hey!\r\n",
20/// )
21/// .unwrap();
22///
23/// assert_eq!(email.subject.unwrap(), "Example Email");
24/// assert_eq!(email.sender.name.unwrap(), vec!["Mubelotix"]);
25/// assert_eq!(email.sender.address.local_part, "mubelotix");
26/// assert_eq!(email.sender.address.domain, "mubelotix.dev");
27/// ```
28#[derive(Debug)]
29pub struct Email<'a> {
30    /// The ASCII text of the body.
31    #[cfg(not(feature = "mime"))]
32    pub body: Option<Cow<'a, str>>,
33
34    #[cfg(feature = "from")]
35    /// The list of authors of the message.\
36    /// It's **not** the identity of the sender. See the [sender field](#structfield.sender).
37    pub from: Vec<Mailbox<'a>>,
38
39    #[cfg(feature = "sender")]
40    /// The mailbox of the agent responsible for the actual transmission of the message.\
41    /// Do not mix up with the [from field](#structfield.from) that contains the list of authors.\
42    /// When there is only one author, this field can be omitted, and its value is inferred. Otherwise, an explicit value is required.
43    pub sender: Mailbox<'a>,
44
45    #[cfg(feature = "subject")]
46    /// A short optional string identifying the topic of the message.
47    pub subject: Option<Cow<'a, str>>,
48
49    #[cfg(feature = "date")]
50    /// The date and time at which the [sender](#structfield.sender) of the message indicated that the message was complete and ready to enter the mail delivery system.
51    /// For instance, this might be the time that a user pushes the "send" or "submit" button in an application program.
52    pub date: DateTime,
53
54    #[cfg(feature = "to")]
55    pub to: Option<Vec<Address<'a>>>,
56
57    #[cfg(feature = "cc")]
58    pub cc: Option<Vec<Address<'a>>>,
59
60    #[cfg(feature = "bcc")]
61    pub bcc: Option<Vec<Address<'a>>>,
62
63    #[cfg(feature = "message-id")]
64    pub message_id: Option<(Cow<'a, str>, Cow<'a, str>)>,
65
66    #[cfg(feature = "in-reply-to")]
67    pub in_reply_to: Option<Vec<(Cow<'a, str>, Cow<'a, str>)>>,
68
69    #[cfg(feature = "references")]
70    pub references: Option<Vec<(Cow<'a, str>, Cow<'a, str>)>>,
71
72    #[cfg(feature = "reply-to")]
73    pub reply_to: Option<Vec<Address<'a>>>,
74
75    #[cfg(feature = "comments")]
76    pub comments: Vec<Cow<'a, str>>,
77
78    #[cfg(feature = "keywords")]
79    pub keywords: Vec<Vec<Cow<'a, str>>>,
80
81    #[cfg(feature = "trace")]
82    pub trace: Vec<(
83        Option<Option<EmailAddress<'a>>>,
84        Vec<(Vec<crate::parsing::fields::ReceivedToken<'a>>, DateTime)>,
85        Vec<crate::parsing::fields::TraceField<'a>>,
86    )>,
87
88    #[cfg(feature = "mime")]
89    pub mime_entity: RawEntity<'a>,
90
91    /// The list of unrecognized fields.\
92    /// Each field is stored as a `(name, value)` tuple.
93    pub unknown_fields: Vec<(&'a str, Cow<'a, str>)>,
94}
95
96impl<'a> Email<'a> {
97    /// Parse an email.
98    pub fn parse(data: &'a [u8]) -> Result<Email<'a>, Error> {
99        let (fields, body) = crate::parse_message(data)?;
100
101        #[cfg(feature = "from")]
102        let mut from = None;
103        #[cfg(feature = "sender")]
104        let mut sender = None;
105        #[cfg(feature = "subject")]
106        let mut subject = None;
107        #[cfg(feature = "date")]
108        let mut date = None;
109        #[cfg(feature = "to")]
110        let mut to = None;
111        #[cfg(feature = "cc")]
112        let mut cc = None;
113        #[cfg(feature = "bcc")]
114        let mut bcc = None;
115        #[cfg(feature = "message-id")]
116        let mut message_id = None;
117        #[cfg(feature = "in-reply-to")]
118        let mut in_reply_to = None;
119        #[cfg(feature = "references")]
120        let mut references = None;
121        #[cfg(feature = "reply-to")]
122        let mut reply_to = None;
123        #[cfg(feature = "comments")]
124        let mut comments = Vec::new();
125        #[cfg(feature = "keywords")]
126        let mut keywords = Vec::new();
127        #[cfg(feature = "trace")]
128        let mut trace = Vec::new();
129        #[cfg(feature = "mime")]
130        let mut mime_version = None;
131        #[cfg(feature = "mime")]
132        let mut content_type = None;
133        #[cfg(feature = "mime")]
134        let mut content_transfer_encoding = None;
135        #[cfg(feature = "mime")]
136        let mut content_id = None;
137        #[cfg(feature = "mime")]
138        let mut content_description = None;
139        #[cfg(feature = "content-disposition")]
140        let mut content_disposition = None;
141
142        let mut unknown_fields = Vec::new();
143
144        for field in fields {
145            match field {
146                #[cfg(feature = "from")]
147                Field::From(mailboxes) => {
148                    if from.is_none() {
149                        from = Some(mailboxes)
150                    } else {
151                        return Err(Error::DuplicateHeader("From"));
152                    }
153                }
154                #[cfg(feature = "sender")]
155                Field::Sender(mailbox) => {
156                    if sender.is_none() {
157                        sender = Some(mailbox)
158                    } else {
159                        return Err(Error::DuplicateHeader("Sender"));
160                    }
161                }
162                #[cfg(feature = "subject")]
163                Field::Subject(data) => {
164                    if subject.is_none() {
165                        subject = Some(data)
166                    } else {
167                        return Err(Error::DuplicateHeader("Subject"));
168                    }
169                }
170                #[cfg(feature = "date")]
171                Field::Date(data) => {
172                    if date.is_none() {
173                        date = Some(data)
174                    } else {
175                        return Err(Error::DuplicateHeader("Date"));
176                    }
177                }
178                #[cfg(feature = "to")]
179                Field::To(addresses) => {
180                    if to.is_none() {
181                        to = Some(addresses)
182                    } else {
183                        return Err(Error::DuplicateHeader("To"));
184                    }
185                }
186                #[cfg(feature = "cc")]
187                Field::Cc(addresses) => {
188                    if cc.is_none() {
189                        cc = Some(addresses)
190                    } else {
191                        return Err(Error::DuplicateHeader("Cc"));
192                    }
193                }
194                #[cfg(feature = "bcc")]
195                Field::Bcc(addresses) => {
196                    if bcc.is_none() {
197                        bcc = Some(addresses)
198                    } else {
199                        return Err(Error::DuplicateHeader("Bcc"));
200                    }
201                }
202                #[cfg(feature = "message-id")]
203                Field::MessageId(id) => {
204                    if message_id.is_none() {
205                        message_id = Some(id)
206                    } else {
207                        return Err(Error::DuplicateHeader("Message-ID"));
208                    }
209                }
210                #[cfg(feature = "in-reply-to")]
211                Field::InReplyTo(ids) => {
212                    if in_reply_to.is_none() {
213                        in_reply_to = Some(ids)
214                    } else {
215                        return Err(Error::DuplicateHeader("In-Reply-To"));
216                    }
217                }
218                #[cfg(feature = "references")]
219                Field::References(ids) => {
220                    if references.is_none() {
221                        references = Some(ids)
222                    } else {
223                        return Err(Error::DuplicateHeader("References"));
224                    }
225                }
226                #[cfg(feature = "reply-to")]
227                Field::ReplyTo(mailboxes) => {
228                    if reply_to.is_none() {
229                        reply_to = Some(mailboxes)
230                    } else {
231                        return Err(Error::DuplicateHeader("Reply-To"));
232                    }
233                }
234                #[cfg(feature = "comments")]
235                Field::Comments(data) => comments.push(data),
236                #[cfg(feature = "keywords")]
237                Field::Keywords(mut data) => {
238                    keywords.append(&mut data);
239                }
240                #[cfg(feature = "trace")]
241                Field::Trace {
242                    return_path,
243                    received,
244                    fields,
245                } => {
246                    trace.push((return_path, received, fields));
247                }
248                #[cfg(feature = "mime")]
249                Field::MimeVersion(major, minor) => {
250                    if mime_version.is_none() {
251                        mime_version = Some((major, minor))
252                    } else {
253                        return Err(Error::DuplicateHeader("Mime-Version"));
254                    }
255                }
256                #[cfg(feature = "mime")]
257                Field::ContentType {
258                    mime_type,
259                    subtype,
260                    parameters,
261                } => {
262                    if content_type.is_none() {
263                        content_type = Some((mime_type, subtype, parameters))
264                    } else {
265                        return Err(Error::DuplicateHeader("Content-Type"));
266                    }
267                }
268                #[cfg(feature = "mime")]
269                Field::ContentTransferEncoding(encoding) => {
270                    if content_transfer_encoding.is_none() {
271                        content_transfer_encoding = Some(encoding)
272                    } else {
273                        return Err(Error::DuplicateHeader("Content-Transfer-Encoding"));
274                    }
275                }
276                #[cfg(feature = "mime")]
277                Field::ContentId(id) => {
278                    if content_id.is_none() {
279                        content_id = Some(id)
280                    } else {
281                        return Err(Error::DuplicateHeader("Content-Id"));
282                    }
283                }
284                #[cfg(feature = "mime")]
285                Field::ContentDescription(description) => {
286                    if content_description.is_none() {
287                        content_description = Some(description)
288                    } else {
289                        return Err(Error::DuplicateHeader("Content-Description"));
290                    }
291                }
292                #[cfg(feature = "content-disposition")]
293                Field::ContentDisposition(disposition) => {
294                    if content_disposition.is_none() {
295                        content_disposition = Some(disposition)
296                    } else {
297                        return Err(Error::DuplicateHeader("Content-Disposition"));
298                    }
299                }
300                Field::Unknown { name, value } => {
301                    unknown_fields.push((name, value));
302                }
303            }
304        }
305
306        #[cfg(feature = "from")]
307        let from = from.ok_or(Error::MissingHeader("From"))?;
308        #[cfg(feature = "date")]
309        let date = date.ok_or(Error::MissingHeader("Date"))?;
310
311        #[cfg(feature = "sender")]
312        let sender = match sender {
313            Some(sender) => sender,
314            None => {
315                if from.len() == 1 {
316                    from[0].clone()
317                } else {
318                    return Err(Error::MissingHeader("Sender"));
319                }
320            }
321        };
322
323        #[cfg(feature = "mime")]
324        let (content_type, body) = (
325            content_type.unwrap_or((
326                ContentType::Text,
327                Cow::Borrowed("plain"),
328                vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]
329                    .into_iter()
330                    .collect(),
331            )),
332            if let Some(body) = body {
333                Some(crate::parsing::mime::entity::decode_value(
334                    Cow::Borrowed(body),
335                    content_transfer_encoding.unwrap_or(ContentTransferEncoding::SevenBit),
336                )?)
337            } else {
338                None
339            },
340        );
341
342        Ok(Email {
343            #[cfg(not(feature = "mime"))]
344            body,
345            #[cfg(feature = "from")]
346            from,
347            #[cfg(feature = "sender")]
348            sender,
349            #[cfg(feature = "subject")]
350            subject,
351            #[cfg(feature = "date")]
352            date,
353            #[cfg(feature = "to")]
354            to,
355            #[cfg(feature = "cc")]
356            cc,
357            #[cfg(feature = "bcc")]
358            bcc,
359            #[cfg(feature = "message-id")]
360            message_id,
361            #[cfg(feature = "in-reply-to")]
362            in_reply_to,
363            #[cfg(feature = "references")]
364            references,
365            #[cfg(feature = "reply-to")]
366            reply_to,
367            #[cfg(feature = "trace")]
368            trace,
369            #[cfg(feature = "comments")]
370            comments,
371            #[cfg(feature = "keywords")]
372            keywords,
373            #[cfg(feature = "mime")]
374            mime_entity: RawEntity {
375                mime_type: content_type.0,
376                subtype: content_type.1,
377                description: content_description,
378                id: content_id,
379                parameters: content_type.2,
380                #[cfg(feature = "content-disposition")]
381                disposition: content_disposition,
382                value: body.unwrap_or(Cow::Borrowed(b"")),
383                additional_headers: Vec::new(),
384            },
385            unknown_fields,
386        })
387    }
388}
389
390impl<'a> std::convert::TryFrom<&'a [u8]> for Email<'a> {
391    type Error = crate::error::Error;
392
393    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
394        Self::parse(value)
395    }
396}
397
398#[cfg(test)]
399mod test {
400    use super::*;
401
402    #[test]
403    fn test_full_email() {
404        /*let multipart = Email::parse(include_bytes!("../mail.txt")).unwrap().mime_entity.parse().unwrap();
405        println!("{:?}", multipart);
406        if let Entity::Multipart{content, subtype: _} = multipart {
407            for entity in content {
408                println!("{:?}", entity.parse().unwrap())
409            }
410        } else {
411            panic!("Failed to parse multipart");
412        }*/
413    }
414
415    #[test]
416    fn test_field_number() {
417        assert!(Email::parse(
418            // missing date
419            b"\
420            From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
421            \r\n\
422            Hey!\r\n",
423        )
424        .is_err());
425
426        assert!(Email::parse(
427            // 2 date fields
428            b"\
429            From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
430            Date: 5 May 2003 18:58:34 +0000\r\n\
431            Date: 6 May 2003 18:58:34 +0000\r\n\
432            \r\n\
433            Hey!\r\n",
434        )
435        .is_err());
436
437        assert!(Email::parse(
438            // missing from
439            b"\
440            Date: 5 May 2003 18:58:34 +0000\r\n\
441            \r\n\
442            Hey!\r\n",
443        )
444        .is_err());
445
446        assert!(Email::parse(
447            // 2 from fields
448            b"\
449            From: Mubelotix <mubelotix@mubelotix.dev>\r\n\
450            From: Someone <jack@gmail.com>\r\n\
451            Date: 5 May 2003 18:58:34 +0000\r\n\
452            \r\n\
453            Hey!\r\n",
454        )
455        .is_err());
456    }
457}