Skip to main content

mail_parser/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6#![doc = include_str!("../README.md")]
7#![deny(rust_2018_idioms)]
8#[forbid(unsafe_code)]
9pub mod core;
10pub mod decoders;
11pub mod mailbox;
12pub mod parsers;
13
14use parsers::MessageStream;
15use std::{borrow::Cow, collections::HashMap, hash::Hash, net::IpAddr};
16
17/// RFC5322/RFC822 message parser.
18#[derive(Debug, PartialEq, Eq, Clone)]
19#[allow(unpredictable_function_pointer_comparisons)]
20pub struct MessageParser {
21    pub(crate) header_map: HashMap<HeaderName<'static>, HdrParseFnc>,
22    pub(crate) def_hdr_parse_fnc: HdrParseFnc,
23}
24
25pub(crate) type HdrParseFnc = for<'x> fn(&mut MessageStream<'x>) -> crate::HeaderValue<'x>;
26
27/// An RFC5322/RFC822 message.
28#[derive(Debug, Default, PartialEq, Clone)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[cfg_attr(
31    feature = "rkyv",
32    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
33)]
34pub struct Message<'x> {
35    #[cfg_attr(feature = "serde", serde(default))]
36    pub html_body: Vec<MessagePartId>,
37    #[cfg_attr(feature = "serde", serde(default))]
38    pub text_body: Vec<MessagePartId>,
39    #[cfg_attr(feature = "serde", serde(default))]
40    pub attachments: Vec<MessagePartId>,
41
42    #[cfg_attr(feature = "serde", serde(default))]
43    pub parts: Vec<MessagePart<'x>>,
44
45    #[cfg_attr(feature = "serde", serde(skip))]
46    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))]
47    pub raw_message: Cow<'x, [u8]>,
48}
49
50/// MIME Message Part
51#[derive(Debug, PartialEq, Default, Clone)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53#[cfg_attr(
54    feature = "rkyv",
55    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
56)]
57pub struct MessagePart<'x> {
58    #[cfg_attr(feature = "serde", serde(default))]
59    pub headers: Vec<Header<'x>>,
60    pub is_encoding_problem: bool,
61    #[cfg_attr(feature = "serde", serde(default))]
62    //#[cfg_attr(feature = "rkyv", rkyv(omit_bounds))]
63    pub body: PartType<'x>,
64    #[cfg_attr(feature = "serde", serde(skip))]
65    pub encoding: Encoding,
66    pub offset_header: u32,
67    pub offset_body: u32,
68    pub offset_end: u32,
69}
70
71/// MIME Part encoding type
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74#[cfg_attr(
75    feature = "rkyv",
76    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
77)]
78#[repr(u8)]
79pub enum Encoding {
80    #[default]
81    None = 0,
82    QuotedPrintable = 1,
83    Base64 = 2,
84}
85
86impl From<u8> for Encoding {
87    fn from(v: u8) -> Self {
88        match v {
89            1 => Encoding::QuotedPrintable,
90            2 => Encoding::Base64,
91            _ => Encoding::None,
92        }
93    }
94}
95
96/// Unique ID representing a MIME part within a message.
97pub type MessagePartId = u32;
98
99/// A text, binary or nested e-mail MIME message part.
100///
101/// - Text: Any text/* part
102/// - Binary: Any other part type that is not text.
103/// - Message: Nested RFC5322 message.
104/// - MultiPart: Multipart part.
105///
106#[derive(Debug, PartialEq, Clone)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108#[cfg_attr(
109    feature = "rkyv",
110    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
111)]
112#[cfg_attr(
113    feature = "rkyv",
114    rkyv(serialize_bounds(
115        __S: rkyv::ser::Writer + rkyv::ser::Allocator,
116        __S::Error: rkyv::rancor::Source,
117    ))
118)]
119#[cfg_attr(
120    feature = "rkyv",
121    rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source))
122)]
123#[cfg_attr(
124    feature = "rkyv",
125    rkyv(bytecheck(
126        bounds(
127            __C: rkyv::validation::ArchiveContext,
128        )
129    ))
130)]
131pub enum PartType<'x> {
132    /// Any text/* part
133    Text(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, str>),
134
135    /// A text/html part
136    Html(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, str>),
137
138    /// Any other part type that is not text.
139    Binary(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, [u8]>),
140
141    /// Any inline binary data that.
142    InlineBinary(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, [u8]>),
143
144    /// Nested RFC5322 message.
145    Message(#[cfg_attr(feature = "rkyv", rkyv(omit_bounds))] Message<'x>),
146
147    /// Multipart part
148    Multipart(Vec<MessagePartId>),
149}
150
151impl Default for PartType<'_> {
152    fn default() -> Self {
153        PartType::Multipart(Vec::with_capacity(0))
154    }
155}
156
157/// An RFC5322 or RFC2369 internet address.
158#[derive(Debug, PartialEq, Eq, Clone)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160#[cfg_attr(
161    feature = "rkyv",
162    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
163)]
164pub struct Addr<'x> {
165    /// The address name including comments
166    #[cfg_attr(feature = "serde", serde(default))]
167    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
168    pub name: Option<Cow<'x, str>>,
169
170    /// An e-mail address (RFC5322/RFC2369) or URL (RFC2369)
171    #[cfg_attr(feature = "serde", serde(default))]
172    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
173    pub address: Option<Cow<'x, str>>,
174}
175
176/// An RFC5322 address group.
177#[derive(Debug, PartialEq, Eq, Clone)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179#[cfg_attr(
180    feature = "rkyv",
181    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
182)]
183pub struct Group<'x> {
184    /// Group name
185    #[cfg_attr(feature = "serde", serde(default))]
186    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
187    pub name: Option<Cow<'x, str>>,
188
189    /// Addresses member of the group
190    #[cfg_attr(feature = "serde", serde(default))]
191    pub addresses: Vec<Addr<'x>>,
192}
193
194/// A message header.
195#[derive(Debug, PartialEq, Eq, Clone)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197#[cfg_attr(
198    feature = "rkyv",
199    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
200)]
201#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
202pub struct Header<'x> {
203    pub name: HeaderName<'x>,
204    pub value: HeaderValue<'x>,
205    pub offset_field: u32,
206    pub offset_start: u32,
207    pub offset_end: u32,
208}
209
210/// A header field
211#[derive(Debug, Clone, PartialOrd, Ord)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
214#[cfg_attr(
215    feature = "rkyv",
216    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
217)]
218#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
219#[non_exhaustive]
220pub enum HeaderName<'x> {
221    Subject,
222    From,
223    To,
224    Cc,
225    Date,
226    Bcc,
227    ReplyTo,
228    Sender,
229    Comments,
230    InReplyTo,
231    Keywords,
232    Received,
233    MessageId,
234    References,
235    ReturnPath,
236    MimeVersion,
237    ContentDescription,
238    ContentId,
239    ContentLanguage,
240    ContentLocation,
241    ContentTransferEncoding,
242    ContentType,
243    ContentDisposition,
244    ResentTo,
245    ResentFrom,
246    ResentBcc,
247    ResentCc,
248    ResentSender,
249    ResentDate,
250    ResentMessageId,
251    ListArchive,
252    ListHelp,
253    ListId,
254    ListOwner,
255    ListPost,
256    ListSubscribe,
257    ListUnsubscribe,
258    Other(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, str>),
259    DkimSignature,
260    ArcAuthenticationResults,
261    ArcMessageSignature,
262    ArcSeal,
263    Dkim2Signature,
264    MessageInstance,
265}
266
267/// Parsed header value.
268#[derive(Debug, PartialEq, Eq, Clone, Default)]
269#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
270#[cfg_attr(
271    feature = "rkyv",
272    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
273)]
274pub enum HeaderValue<'x> {
275    /// Address list or group
276    Address(Address<'x>),
277
278    /// String
279    Text(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, str>),
280
281    /// List of strings
282    TextList(
283        #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
284        Vec<Cow<'x, str>>,
285    ),
286
287    /// Datetime
288    DateTime(DateTime),
289
290    /// Content-Type or Content-Disposition header
291    ContentType(ContentType<'x>),
292
293    /// Received header
294    Received(Box<Received<'x>>),
295
296    #[default]
297    Empty,
298}
299
300#[derive(Debug, PartialEq, Eq, Clone)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302#[cfg_attr(
303    feature = "rkyv",
304    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
305)]
306pub enum Address<'x> {
307    /// Address list
308    List(Vec<Addr<'x>>),
309    /// Group of addresses
310    Group(Vec<Group<'x>>),
311}
312
313/// Header form
314#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
315pub enum HeaderForm {
316    Raw,
317    Text,
318    Addresses,
319    GroupedAddresses,
320    MessageIds,
321    Date,
322    URLs,
323}
324/// An RFC2047 Content-Type or RFC2183 Content-Disposition MIME header field.
325#[derive(Debug, PartialEq, Eq, Clone)]
326#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327#[cfg_attr(
328    feature = "rkyv",
329    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
330)]
331pub struct ContentType<'x> {
332    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))]
333    pub c_type: Cow<'x, str>,
334    #[cfg_attr(feature = "serde", serde(default))]
335    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
336    pub c_subtype: Option<Cow<'x, str>>,
337    #[cfg_attr(feature = "serde", serde(default))]
338    pub attributes: Option<Vec<Attribute<'x>>>,
339}
340
341#[derive(Debug, PartialEq, Eq, Clone)]
342#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
343#[cfg_attr(
344    feature = "rkyv",
345    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
346)]
347pub struct Attribute<'x> {
348    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))]
349    pub name: Cow<'x, str>,
350    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))]
351    pub value: Cow<'x, str>,
352}
353
354/// An RFC5322 datetime.
355#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
356#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
357#[cfg_attr(
358    feature = "rkyv",
359    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
360)]
361pub struct DateTime {
362    pub year: u16,
363    pub month: u8,
364    pub day: u8,
365    pub hour: u8,
366    pub minute: u8,
367    pub second: u8,
368    pub tz_before_gmt: bool,
369    pub tz_hour: u8,
370    pub tz_minute: u8,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Default)]
374#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
375#[cfg_attr(
376    feature = "rkyv",
377    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
378)]
379pub struct Received<'x> {
380    #[cfg_attr(feature = "serde", serde(default))]
381    pub from: Option<Host<'x>>,
382    #[cfg_attr(feature = "serde", serde(default))]
383    pub from_ip: Option<IpAddr>,
384    #[cfg_attr(feature = "serde", serde(default))]
385    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
386    pub from_iprev: Option<Cow<'x, str>>,
387    #[cfg_attr(feature = "serde", serde(default))]
388    pub by: Option<Host<'x>>,
389    #[cfg_attr(feature = "serde", serde(default))]
390    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
391    pub for_: Option<Cow<'x, str>>,
392    #[cfg_attr(feature = "serde", serde(default))]
393    pub with: Option<Protocol>,
394    #[cfg_attr(feature = "serde", serde(default))]
395    pub tls_version: Option<TlsVersion>,
396    #[cfg_attr(feature = "serde", serde(default))]
397    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
398    pub tls_cipher: Option<Cow<'x, str>>,
399    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
400    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
401    pub id: Option<Cow<'x, str>>,
402    #[cfg_attr(feature = "serde", serde(default))]
403    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
404    pub ident: Option<Cow<'x, str>>,
405    #[cfg_attr(feature = "serde", serde(default))]
406    pub helo: Option<Host<'x>>,
407    #[cfg_attr(feature = "serde", serde(default))]
408    pub helo_cmd: Option<Greeting>,
409    #[cfg_attr(feature = "serde", serde(default))]
410    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
411    pub via: Option<Cow<'x, str>>,
412    pub date: Option<DateTime>,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
417#[cfg_attr(
418    feature = "rkyv",
419    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
420)]
421pub enum Host<'x> {
422    Name(#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::AsOwned))] Cow<'x, str>),
423    IpAddr(IpAddr),
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
428#[cfg_attr(
429    feature = "rkyv",
430    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
431)]
432pub enum TlsVersion {
433    SSLv2,
434    SSLv3,
435    TLSv1_0,
436    TLSv1_1,
437    TLSv1_2,
438    TLSv1_3,
439    DTLSv1_0,
440    DTLSv1_2,
441    DTLSv1_3,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
446#[cfg_attr(
447    feature = "rkyv",
448    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
449)]
450pub enum Greeting {
451    Helo,
452    Ehlo,
453    Lhlo,
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
458#[cfg_attr(
459    feature = "rkyv",
460    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
461)]
462#[allow(clippy::upper_case_acronyms)]
463pub enum Protocol {
464    // IANA Mail Transmission Types
465    SMTP,
466    ESMTP,
467    ESMTPA,
468    ESMTPS,
469    ESMTPSA,
470    LMTP,
471    LMTPA,
472    LMTPS,
473    LMTPSA,
474    MMS,
475    UTF8SMTP,
476    UTF8SMTPA,
477    UTF8SMTPS,
478    UTF8SMTPSA,
479    UTF8LMTP,
480    UTF8LMTPA,
481    UTF8LMTPS,
482    UTF8LMTPSA,
483
484    // Non-Standard Mail Transmission Types
485    HTTP,
486    HTTPS,
487    IMAP,
488    POP3,
489    Local, // includes stdin, socket, etc.
490}
491
492/// MIME Header field access trait
493pub trait MimeHeaders<'x> {
494    /// Returns the Content-Description field
495    fn content_description(&self) -> Option<&str>;
496    /// Returns the Content-Disposition field
497    fn content_disposition(&self) -> Option<&ContentType<'_>>;
498    /// Returns the Content-ID field
499    fn content_id(&self) -> Option<&str>;
500    /// Returns the Content-Encoding field
501    fn content_transfer_encoding(&self) -> Option<&str>;
502    /// Returns the Content-Type field
503    fn content_type(&self) -> Option<&ContentType<'_>>;
504    /// Returns the Content-Language field
505    fn content_language(&self) -> &HeaderValue<'_>;
506    /// Returns the Content-Location field
507    fn content_location(&self) -> Option<&str>;
508    /// Returns the attachment name, if any.
509    fn attachment_name(&self) -> Option<&str> {
510        self.content_disposition()
511            .and_then(|cd| cd.attribute("filename"))
512            .or_else(|| self.content_type().and_then(|ct| ct.attribute("name")))
513    }
514    // Returns true is the content type matches
515    fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
516        self.content_type().is_some_and(|ct| {
517            ct.c_type.eq_ignore_ascii_case(type_)
518                && ct
519                    .c_subtype
520                    .as_ref()
521                    .is_some_and(|st| st.eq_ignore_ascii_case(subtype))
522        })
523    }
524}
525
526pub trait GetHeader<'x> {
527    fn header_value(&self, name: &HeaderName<'_>) -> Option<&HeaderValue<'x>>;
528    fn header(&self, name: impl Into<HeaderName<'x>>) -> Option<&Header<'x>>;
529}
530
531struct BodyPartIterator<'x> {
532    message: &'x Message<'x>,
533    list: &'x [MessagePartId],
534    pos: i32,
535}
536
537struct AttachmentIterator<'x> {
538    message: &'x Message<'x>,
539    pos: i32,
540}