Skip to main content

eml_codec/mime/
mod.rs

1/// MIME specific headers
2pub mod field;
3
4/// Transfer-Encoding representation
5pub mod mechanism;
6
7/// Content-Type representation
8pub mod r#type;
9
10#[cfg(feature = "arbitrary")]
11use arbitrary::Arbitrary;
12use bounded_static::ToStatic;
13use std::collections::HashSet;
14
15#[cfg(feature = "arbitrary")]
16use crate::fuzz_eq::FuzzEq;
17use crate::i18n::ContainsUtf8;
18use crate::imf::identification::MessageID;
19use crate::mime::field::NaiveField;
20use crate::mime::mechanism::Mechanism;
21use crate::mime::r#type::{AnyType, MessageSubtype, NaiveType};
22use crate::text::misc_token::Unstructured;
23use crate::utils::set_opt;
24
25#[derive(Debug, Default, PartialEq, Clone, ToStatic)]
26#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
27pub struct CommonMIME<'a> {
28    pub transfer_encoding: Mechanism<'a>,
29    pub id: Option<MessageID<'a>>,
30    pub description: Option<Unstructured<'a>>,
31}
32
33impl<'a> ContainsUtf8 for CommonMIME<'a> {
34    fn contains_utf8(&self) -> bool {
35        self.transfer_encoding.contains_utf8()
36            || self.id.contains_utf8()
37            || self.description.contains_utf8()
38    }
39}
40
41// Invariant: when T is mime::r#type::Multipart or mime::r#type::Message,
42// fields.transfer_encoding must be 7bit, 8bit or binary.
43#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic)]
44#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
45pub struct MIME<'a, T> {
46    pub ctype: T,
47    pub fields: CommonMIME<'a>,
48}
49
50#[cfg(feature = "arbitrary")]
51impl<'a> Arbitrary<'a> for MIME<'a, r#type::Multipart<'a>> {
52    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
53        let mime = MIME {
54            ctype: u.arbitrary()?,
55            fields: u.arbitrary()?,
56        };
57        match mime.fields.transfer_encoding {
58            Mechanism::_7Bit | Mechanism::_8Bit | Mechanism::Binary => (),
59            _ => return Err(arbitrary::Error::IncorrectFormat),
60        };
61        Ok(mime)
62    }
63}
64
65#[cfg(feature = "arbitrary")]
66impl<'a> Arbitrary<'a> for MIME<'a, r#type::Message<'a>> {
67    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
68        let mime = MIME {
69            ctype: u.arbitrary()?,
70            fields: u.arbitrary()?,
71        };
72        match mime.fields.transfer_encoding {
73            Mechanism::_7Bit | Mechanism::_8Bit | Mechanism::Binary => (),
74            _ => return Err(arbitrary::Error::IncorrectFormat),
75        };
76        Ok(mime)
77    }
78}
79
80#[cfg(feature = "arbitrary")]
81impl<'a> Arbitrary<'a> for MIME<'a, r#type::Text<'a>> {
82    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
83        Ok(MIME {
84            ctype: u.arbitrary()?,
85            fields: u.arbitrary()?,
86        })
87    }
88}
89
90#[cfg(feature = "arbitrary")]
91impl<'a> Arbitrary<'a> for MIME<'a, r#type::Binary<'a>> {
92    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
93        Ok(MIME {
94            ctype: u.arbitrary()?,
95            fields: u.arbitrary()?,
96        })
97    }
98}
99
100#[derive(Debug, PartialEq, Clone, ToStatic)]
101#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
102pub enum AnyMIME<'a> {
103    Mult(MIME<'a, r#type::Multipart<'a>>),
104    Msg(MIME<'a, r#type::Message<'a>>),
105    Txt(MIME<'a, r#type::Text<'a>>),
106    Bin(MIME<'a, r#type::Binary<'a>>),
107}
108impl<'a> AnyMIME<'a> {
109    pub fn ctype(&self) -> AnyType<'a> {
110        match self {
111            Self::Mult(m) => AnyType::Multipart(m.ctype.clone()),
112            Self::Msg(m) => AnyType::Message(m.ctype.clone()),
113            Self::Txt(m) => AnyType::Text(m.ctype.clone()),
114            Self::Bin(m) => AnyType::Binary(m.ctype.clone()),
115        }
116    }
117
118    pub fn common(&self) -> &CommonMIME<'a> {
119        match self {
120            Self::Mult(v) => &v.fields,
121            Self::Msg(v) => &v.fields,
122            Self::Txt(v) => &v.fields,
123            Self::Bin(v) => &v.fields,
124        }
125    }
126
127    pub fn get_field(&self, f: field::Entry) -> Option<field::Field<'a>> {
128        match f {
129            field::Entry::Type => Some(field::Field::Type(self.ctype())),
130            field::Entry::TransferEncoding => Some(field::Field::TransferEncoding(
131                self.common().transfer_encoding.clone(),
132            )),
133            field::Entry::ID => self.common().id.clone().map(field::Field::ID),
134            field::Entry::Description => self
135                .common()
136                .description
137                .clone()
138                .map(field::Field::Description),
139        }
140    }
141
142    // Returns the list of entries included in this MIME struct. This is used to
143    // define the Arbitrary instance for Message and AnyPart, to construct a
144    // randomly ordered list of field entries.
145    pub fn field_entries(&self) -> HashSet<field::Entry> {
146        let mut fs = HashSet::default();
147        fs.insert(field::Entry::Type);
148        fs.insert(field::Entry::TransferEncoding);
149        let common = self.common();
150        if common.id.is_some() {
151            fs.insert(field::Entry::ID);
152        }
153        if common.description.is_some() {
154            fs.insert(field::Entry::Description);
155        }
156        fs
157    }
158}
159impl<'a> ContainsUtf8 for AnyMIME<'a> {
160    fn contains_utf8(&self) -> bool {
161        match self {
162            Self::Mult(v) => v.contains_utf8(),
163            Self::Msg(v) => v.contains_utf8(),
164            Self::Txt(v) => v.contains_utf8(),
165            Self::Bin(v) => v.contains_utf8(),
166        }
167    }
168}
169
170impl<'a> From<MIME<'a, r#type::Multipart<'a>>> for AnyMIME<'a> {
171    fn from(val: MIME<'a, r#type::Multipart<'a>>) -> Self {
172        AnyMIME::Mult(val)
173    }
174}
175
176impl<'a> From<MIME<'a, r#type::Message<'a>>> for AnyMIME<'a> {
177    fn from(val: MIME<'a, r#type::Message<'a>>) -> Self {
178        AnyMIME::Msg(val)
179    }
180}
181
182impl<'a> From<MIME<'a, r#type::Text<'a>>> for AnyMIME<'a> {
183    fn from(val: MIME<'a, r#type::Text<'a>>) -> Self {
184        AnyMIME::Txt(val)
185    }
186}
187impl<'a> From<MIME<'a, r#type::Binary<'a>>> for AnyMIME<'a> {
188    fn from(val: MIME<'a, r#type::Binary<'a>>) -> Self {
189        AnyMIME::Bin(val)
190    }
191}
192
193#[derive(Clone, Debug, Default, PartialEq, ToStatic)]
194#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
195pub struct NaiveMIME<'a> {
196    ctype: Option<r#type::NaiveType<'a>>,
197    transfer_encoding: Option<Mechanism<'a>>,
198    id: Option<MessageID<'a>>,
199    description: Option<Unstructured<'a>>,
200}
201
202impl<'a> NaiveMIME<'a> {
203    pub fn add_field(&mut self, f: NaiveField<'a>) -> Option<field::Entry> {
204        match f {
205            NaiveField::Type(ctype) => {
206                set_opt(&mut self.ctype, ctype).then_some(field::Entry::Type)
207            }
208            NaiveField::TransferEncoding(enc) => {
209                set_opt(&mut self.transfer_encoding, enc).then_some(field::Entry::TransferEncoding)
210            }
211            NaiveField::ID(id) => set_opt(&mut self.id, id).then_some(field::Entry::ID),
212            NaiveField::Description(desc) => {
213                set_opt(&mut self.description, desc).then_some(field::Entry::Description)
214            }
215        }
216    }
217
218    pub fn to_interpreted(self, default_type: DefaultType) -> AnyMIME<'a> {
219        let typ: AnyType = self
220            .ctype
221            .as_ref()
222            .map(NaiveType::to_type)
223            .unwrap_or(default_type.to_type());
224        let transfer_encoding = self.transfer_encoding.unwrap_or_default();
225        let mut fields = CommonMIME {
226            transfer_encoding,
227            id: self.id,
228            description: self.description,
229        };
230        match typ {
231            AnyType::Multipart(ctype) => {
232                // Ensure we are using an encoding allowed for multipart
233                fields.transfer_encoding = fields.transfer_encoding.to_multipart_encoding();
234                AnyMIME::Mult(MIME { ctype, fields })
235            }
236            AnyType::Message(ctype) => {
237                // Ensure we are using an encoding allowed for message/rfc822
238                if let MessageSubtype::RFC822 = ctype.subtype {
239                    fields.transfer_encoding =
240                        fields.transfer_encoding.to_message_rfc822_encoding();
241                }
242                // TODO: enforce corresponding restrictions for other message subtypes?
243                AnyMIME::Msg(MIME { ctype, fields })
244            }
245            AnyType::Text(ctype) => AnyMIME::Txt(MIME { ctype, fields }),
246            AnyType::Binary(ctype) => AnyMIME::Bin(MIME { ctype, fields }),
247        }
248    }
249}
250
251#[derive(Default)]
252pub enum DefaultType {
253    #[default]
254    Generic,
255    Digest,
256}
257
258#[expect(clippy::wrong_self_convention)]
259impl DefaultType {
260    fn to_type(self) -> AnyType<'static> {
261        match self {
262            Self::Generic => AnyType::Text(Default::default()),
263            Self::Digest => AnyType::Message(Default::default()),
264        }
265    }
266}