Skip to main content

eml_codec/mime/
field.rs

1use bounded_static::ToStatic;
2use nom::combinator::map;
3#[cfg(feature = "tracing")]
4use tracing::warn;
5
6#[cfg(feature = "arbitrary")]
7use crate::fuzz_eq::FuzzEq;
8use crate::header;
9use crate::imf::identification::{msg_id, MessageID};
10use crate::mime::mechanism::{mechanism, Mechanism};
11use crate::mime::r#type::{naive_type, AnyType, NaiveType};
12use crate::print::{Formatter, Print};
13use crate::text::misc_token::{unstructured, Unstructured};
14#[cfg(feature = "tracing-unsupported")]
15use crate::utils::bytes_to_trace_string;
16
17#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, ToStatic)]
18#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
19pub enum Entry {
20    Type,
21    TransferEncoding,
22    ID,
23    Description,
24}
25
26#[derive(Clone, Debug, PartialEq, ToStatic)]
27#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
28pub enum Field<'a> {
29    Type(AnyType<'a>),
30    TransferEncoding(Mechanism<'a>),
31    ID(MessageID<'a>),
32    Description(Unstructured<'a>),
33}
34
35impl<'a> Field<'a> {
36    pub fn raw_name(&self) -> header::FieldName<'static> {
37        match self {
38            Field::Type(_) => header::FieldName(b"Content-Type".into()),
39            Field::TransferEncoding(_) => header::FieldName(b"Content-Transfer-Encoding".into()),
40            Field::ID(_) => header::FieldName(b"Content-Id".into()),
41            Field::Description(_) => header::FieldName(b"Content-Description".into()),
42        }
43    }
44}
45impl<'a> Print for Field<'a> {
46    fn print(&self, fmt: &mut impl Formatter) {
47        match self {
48            Self::Type(nt) => header::print(fmt, b"Content-Type", nt),
49            Self::TransferEncoding(enc) => header::print(fmt, b"Content-Transfer-Encoding", enc),
50            Self::ID(id) => header::print(fmt, b"Content-Id", id),
51            Self::Description(desc) => {
52                header::print_unstructured(fmt, b"Content-Description", desc)
53            }
54        }
55    }
56}
57
58#[derive(Clone, Debug, PartialEq, ToStatic)]
59pub enum NaiveField<'a> {
60    Type(NaiveType<'a>),
61    TransferEncoding(Mechanism<'a>),
62    ID(MessageID<'a>),
63    Description(Unstructured<'a>),
64}
65
66#[derive(Clone, Copy, Debug)]
67pub enum InvalidField {
68    Name,
69    Body,
70}
71
72impl<'a> TryFrom<&header::FieldRaw<'a>> for NaiveField<'a> {
73    type Error = InvalidField;
74
75    #[cfg_attr(
76        feature = "tracing",
77        tracing::instrument(name = "mime::field::Field::try_from")
78    )]
79    fn try_from(f: &header::FieldRaw<'a>) -> Result<Self, Self::Error> {
80        let content = match f.name.bytes().to_ascii_lowercase().as_slice() {
81            b"content-type" => map(naive_type, NaiveField::Type)(f.body),
82            b"content-transfer-encoding" => map(mechanism, NaiveField::TransferEncoding)(f.body),
83            b"content-id" => map(msg_id, NaiveField::ID)(f.body),
84            b"content-description" => map(unstructured, NaiveField::Description)(f.body),
85            _ => return Err(InvalidField::Name),
86        };
87
88        match content {
89            Ok((b"", content)) => Ok(content),
90            Ok((_rest, _)) => {
91                // return an error if we haven't parsed the full value
92                #[cfg(feature = "tracing-unsupported")]
93                warn!(rest = %bytes_to_trace_string(_rest),
94                      "leftover input after parsing");
95                Err(InvalidField::Body)
96            }
97            Err(_) => Err(InvalidField::Body),
98        }
99    }
100}
101
102pub fn is_mime_header(name: &header::FieldName) -> bool {
103    matches!(
104        name.bytes().to_ascii_lowercase().as_slice(),
105        b"content-type" | b"content-transfer-encoding" | b"content-id" | b"content-description"
106    )
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::header;
113    use crate::mime::r#type::*;
114    use crate::text::misc_token::MIMEWord;
115    use crate::text::quoted::QuotedString;
116    use crate::text::words::MIMEAtom;
117
118    #[test]
119    fn test_header() {
120        let fullmail: &[u8] = r#"Date: Sat, 8 Jul 2023 07:14:29 +0200
121From: Grrrnd Zero <grrrndzero@example.org>
122To: John Doe <jdoe@machine.example>
123Subject: Re: Saying Hello
124Message-ID: <NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>
125MIME-Version: 1.0
126Content-Type: multipart/alternative;
127 boundary="b1_e376dc71bafc953c0b0fdeb9983a9956"
128Content-Transfer-Encoding: 7bit
129
130This is a multipart message.
131
132"#
133        .as_bytes();
134
135        let (input, hdrs) = header::header_kv(fullmail);
136
137        assert_eq!(
138            (input, hdrs.iter().flat_map(NaiveField::try_from).collect()),
139            (
140                &b"This is a multipart message.\n\n"[..],
141                vec![
142                    NaiveField::Type(NaiveType {
143                        main: MIMEAtom(b"multipart"[..].into()),
144                        sub: MIMEAtom(b"alternative"[..].into()),
145                        params: vec![Parameter {
146                            name: MIMEAtom(b"boundary"[..].into()),
147                            value: MIMEWord::Quoted(QuotedString(vec![
148                                "b1_e376dc71bafc953c0b0fdeb9983a9956"[..].into()
149                            ])),
150                        }]
151                    }),
152                    NaiveField::TransferEncoding(Mechanism::_7Bit),
153                ],
154            ),
155        );
156    }
157}