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#[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#[derive(Debug, PartialEq)]
70pub enum AnyType {
71 Multipart(Multipart),
73 Message(Deductible<Message>),
74
75 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#[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}