eml_codec/mime/
type.rs

1use nom::{
2    bytes::complete::tag,
3    combinator::{map, opt},
4    multi::many0,
5    sequence::{preceded, terminated, tuple},
6    IResult,
7};
8use std::fmt;
9
10use crate::mime::charset::EmailCharset;
11use crate::mime::{AnyMIME, NaiveMIME, MIME};
12use crate::text::misc_token::{mime_word, MIMEWord};
13use crate::text::words::mime_atom;
14
15// --------- NAIVE TYPE
16#[derive(PartialEq, Clone)]
17pub struct NaiveType<'a> {
18    pub main: &'a [u8],
19    pub sub: &'a [u8],
20    pub params: Vec<Parameter<'a>>,
21}
22impl<'a> fmt::Debug for NaiveType<'a> {
23    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
24        fmt.debug_struct("mime::NaiveType")
25            .field("main", &String::from_utf8_lossy(self.main))
26            .field("sub", &String::from_utf8_lossy(self.sub))
27            .field("params", &self.params)
28            .finish()
29    }
30}
31impl<'a> NaiveType<'a> {
32    pub fn to_type(&self) -> AnyType {
33        self.into()
34    }
35}
36pub fn naive_type(input: &[u8]) -> IResult<&[u8], NaiveType> {
37    map(
38        tuple((mime_atom, tag("/"), mime_atom, parameter_list)),
39        |(main, _, sub, params)| NaiveType { main, sub, params },
40    )(input)
41}
42
43#[derive(PartialEq, Clone)]
44pub struct Parameter<'a> {
45    pub name: &'a [u8],
46    pub value: MIMEWord<'a>,
47}
48impl<'a> fmt::Debug for Parameter<'a> {
49    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
50        fmt.debug_struct("mime::Parameter")
51            .field("name", &String::from_utf8_lossy(self.name))
52            .field("value", &self.value)
53            .finish()
54    }
55}
56
57pub fn parameter(input: &[u8]) -> IResult<&[u8], Parameter> {
58    map(
59        tuple((mime_atom, tag(b"="), mime_word)),
60        |(name, _, value)| Parameter { name, value },
61    )(input)
62}
63pub fn parameter_list(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
64    terminated(many0(preceded(tag(";"), parameter)), opt(tag(";")))(input)
65}
66
67// MIME TYPES TRANSLATED TO RUST TYPING SYSTEM
68
69#[derive(Debug, PartialEq)]
70pub enum AnyType {
71    // Composite types
72    Multipart(Multipart),
73    Message(Deductible<Message>),
74
75    // Discrete types
76    Text(Deductible<Text>),
77    Binary(Binary),
78}
79
80impl<'a> From<&'a NaiveType<'a>> for AnyType {
81    fn from(nt: &'a NaiveType<'a>) -> Self {
82        match nt.main.to_ascii_lowercase().as_slice() {
83            b"multipart" => Multipart::try_from(nt)
84                .map(Self::Multipart)
85                .unwrap_or(Self::Text(DeductibleText::default())),
86            b"message" => Self::Message(DeductibleMessage::Explicit(Message::from(nt))),
87            b"text" => Self::Text(DeductibleText::Explicit(Text::from(nt))),
88            _ => Self::Binary(Binary::default()),
89        }
90    }
91}
92
93impl<'a> AnyType {
94    pub fn to_mime(self, fields: NaiveMIME<'a>) -> AnyMIME<'a> {
95        match self {
96            Self::Multipart(interpreted_type) => AnyMIME::Mult(MIME::<Multipart> {
97                interpreted_type,
98                fields,
99            }),
100            Self::Message(interpreted_type) => AnyMIME::Msg(MIME::<DeductibleMessage> {
101                interpreted_type,
102                fields,
103            }),
104            Self::Text(interpreted_type) => AnyMIME::Txt(MIME::<DeductibleText> {
105                interpreted_type,
106                fields,
107            }),
108            Self::Binary(interpreted_type) => AnyMIME::Bin(MIME::<Binary> {
109                interpreted_type,
110                fields,
111            }),
112        }
113    }
114}
115
116#[derive(Debug, PartialEq, Clone)]
117pub enum Deductible<T: Default> {
118    Inferred(T),
119    Explicit(T),
120}
121impl<T: Default> Default for Deductible<T> {
122    fn default() -> Self {
123        Self::Inferred(T::default())
124    }
125}
126
127// REAL PARTS
128
129#[derive(Debug, PartialEq, Clone)]
130pub struct Multipart {
131    pub subtype: MultipartSubtype,
132    pub boundary: String,
133}
134impl Multipart {
135    pub fn main_type(&self) -> String {
136        "multipart".into()
137    }
138}
139impl<'a> TryFrom<&'a NaiveType<'a>> for Multipart {
140    type Error = ();
141
142    fn try_from(nt: &'a NaiveType<'a>) -> Result<Self, Self::Error> {
143        nt.params
144            .iter()
145            .find(|x| x.name.to_ascii_lowercase().as_slice() == b"boundary")
146            .map(|boundary| Multipart {
147                subtype: MultipartSubtype::from(nt),
148                boundary: boundary.value.to_string(),
149            })
150            .ok_or(())
151    }
152}
153
154#[derive(Debug, PartialEq, Clone)]
155pub enum MultipartSubtype {
156    Alternative,
157    Mixed,
158    Digest,
159    Parallel,
160    Report,
161    Unknown,
162}
163impl ToString for MultipartSubtype {
164    fn to_string(&self) -> String {
165        match self {
166            Self::Alternative => "alternative",
167            Self::Mixed => "mixed",
168            Self::Digest => "digest",
169            Self::Parallel => "parallel",
170            Self::Report => "report",
171            Self::Unknown => "mixed",
172        }
173        .into()
174    }
175}
176impl<'a> From<&NaiveType<'a>> for MultipartSubtype {
177    fn from(nt: &NaiveType<'a>) -> Self {
178        match nt.sub.to_ascii_lowercase().as_slice() {
179            b"alternative" => Self::Alternative,
180            b"mixed" => Self::Mixed,
181            b"digest" => Self::Digest,
182            b"parallel" => Self::Parallel,
183            b"report" => Self::Report,
184            _ => Self::Unknown,
185        }
186    }
187}
188
189#[derive(Debug, PartialEq, Default, Clone)]
190pub enum MessageSubtype {
191    #[default]
192    RFC822,
193    Partial,
194    External,
195    Unknown,
196}
197impl ToString for MessageSubtype {
198    fn to_string(&self) -> String {
199        match self {
200            Self::RFC822 => "rfc822",
201            Self::Partial => "partial",
202            Self::External => "external",
203            Self::Unknown => "rfc822",
204        }
205        .into()
206    }
207}
208
209pub type DeductibleMessage = Deductible<Message>;
210#[derive(Debug, PartialEq, Default, Clone)]
211pub struct Message {
212    pub subtype: MessageSubtype,
213}
214impl<'a> From<&NaiveType<'a>> for Message {
215    fn from(nt: &NaiveType<'a>) -> Self {
216        match nt.sub.to_ascii_lowercase().as_slice() {
217            b"rfc822" => Self {
218                subtype: MessageSubtype::RFC822,
219            },
220            b"partial" => Self {
221                subtype: MessageSubtype::Partial,
222            },
223            b"external" => Self {
224                subtype: MessageSubtype::External,
225            },
226            _ => Self {
227                subtype: MessageSubtype::Unknown,
228            },
229        }
230    }
231}
232impl From<Deductible<Message>> for Message {
233    fn from(d: Deductible<Message>) -> Self {
234        match d {
235            Deductible::Inferred(t) | Deductible::Explicit(t) => t,
236        }
237    }
238}
239
240pub type DeductibleText = Deductible<Text>;
241#[derive(Debug, PartialEq, Default, Clone)]
242pub struct Text {
243    pub subtype: TextSubtype,
244    pub charset: Deductible<EmailCharset>,
245}
246impl<'a> From<&NaiveType<'a>> for Text {
247    fn from(nt: &NaiveType<'a>) -> Self {
248        Self {
249            subtype: TextSubtype::from(nt),
250            charset: nt
251                .params
252                .iter()
253                .find(|x| x.name.to_ascii_lowercase().as_slice() == b"charset")
254                .map(|x| Deductible::Explicit(EmailCharset::from(x.value.to_string().as_bytes())))
255                .unwrap_or(Deductible::Inferred(EmailCharset::US_ASCII)),
256        }
257    }
258}
259impl From<Deductible<Text>> for Text {
260    fn from(d: Deductible<Text>) -> Self {
261        match d {
262            Deductible::Inferred(t) | Deductible::Explicit(t) => t,
263        }
264    }
265}
266
267#[derive(Debug, PartialEq, Default, Clone)]
268pub enum TextSubtype {
269    #[default]
270    Plain,
271    Html,
272    Unknown,
273}
274impl ToString for TextSubtype {
275    fn to_string(&self) -> String {
276        match self {
277            Self::Plain | Self::Unknown => "plain",
278            Self::Html => "html",
279        }
280        .into()
281    }
282}
283impl<'a> From<&NaiveType<'a>> for TextSubtype {
284    fn from(nt: &NaiveType<'a>) -> Self {
285        match nt.sub.to_ascii_lowercase().as_slice() {
286            b"plain" => Self::Plain,
287            b"html" => Self::Html,
288            _ => Self::Unknown,
289        }
290    }
291}
292
293#[derive(Debug, PartialEq, Default, Clone)]
294pub struct Binary {}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::mime::charset::EmailCharset;
300    use crate::mime::r#type::Deductible;
301    use crate::text::quoted::QuotedString;
302
303    #[test]
304    fn test_parameter() {
305        assert_eq!(
306            parameter(b"charset=utf-8"),
307            Ok((
308                &b""[..],
309                Parameter {
310                    name: &b"charset"[..],
311                    value: MIMEWord::Atom(&b"utf-8"[..]),
312                }
313            )),
314        );
315        assert_eq!(
316            parameter(b"charset=\"utf-8\""),
317            Ok((
318                &b""[..],
319                Parameter {
320                    name: &b"charset"[..],
321                    value: MIMEWord::Quoted(QuotedString(vec![&b"utf-8"[..]])),
322                }
323            )),
324        );
325    }
326
327    #[test]
328    fn test_content_type_plaintext() {
329        let (rest, nt) = naive_type(b"text/plain;\r\n charset=utf-8").unwrap();
330        assert_eq!(rest, &b""[..]);
331
332        assert_eq!(
333            nt.to_type(),
334            AnyType::Text(Deductible::Explicit(Text {
335                charset: Deductible::Explicit(EmailCharset::UTF_8),
336                subtype: TextSubtype::Plain,
337            }))
338        );
339    }
340
341    #[test]
342    fn test_content_type_multipart() {
343        let (rest, nt) = naive_type(b"multipart/mixed;\r\n\tboundary=\"--==_mimepart_64a3f2c69114f_2a13d020975fe\";\r\n\tcharset=UTF-8").unwrap();
344        assert_eq!(rest, &[]);
345        assert_eq!(
346            nt.to_type(),
347            AnyType::Multipart(Multipart {
348                subtype: MultipartSubtype::Mixed,
349                boundary: "--==_mimepart_64a3f2c69114f_2a13d020975fe".into(),
350            })
351        );
352    }
353
354    #[test]
355    fn test_content_type_message() {
356        let (rest, nt) = naive_type(b"message/rfc822").unwrap();
357        assert_eq!(rest, &[]);
358
359        assert_eq!(
360            nt.to_type(),
361            AnyType::Message(Deductible::Explicit(Message {
362                subtype: MessageSubtype::RFC822
363            }))
364        );
365    }
366
367    #[test]
368    fn test_parameter_ascii() {
369        assert_eq!(
370            parameter(b"charset = (simple) us-ascii (Plain text)"),
371            Ok((
372                &b""[..],
373                Parameter {
374                    name: &b"charset"[..],
375                    value: MIMEWord::Atom(&b"us-ascii"[..]),
376                }
377            ))
378        );
379    }
380
381    #[test]
382    fn test_parameter_terminated_with_semi_colon() {
383        assert_eq!(
384            parameter_list(b";boundary=\"festivus\";"),
385            Ok((
386                &b""[..],
387                vec![Parameter {
388                    name: &b"boundary"[..],
389                    value: MIMEWord::Quoted(QuotedString(vec![&b"festivus"[..]])),
390                }],
391            ))
392        );
393    }
394}