mml/message/
interpreter.rs

1//! # MIME to MML message interpretation module
2//!
3//! Module dedicated to MIME → MML message interpretation.
4
5use mail_builder::MessageBuilder;
6use mail_parser::{Message, MessageParser};
7use std::path::PathBuf;
8
9#[cfg(feature = "pgp")]
10use crate::pgp::Pgp;
11use crate::{
12    message::{FilterParts, MimeBodyInterpreter},
13    Error, Result,
14};
15
16use super::header;
17
18/// Filters headers to show in the interpreted message.
19#[derive(Clone, Debug, Default, Eq, PartialEq)]
20pub enum FilterHeaders {
21    /// Include all available headers to the interpreted message.
22    #[default]
23    All,
24
25    /// Include given headers to the interpreted message.
26    Include(Vec<String>),
27
28    /// Exclude given headers from the interpreted message.
29    Exclude(Vec<String>),
30}
31
32impl FilterHeaders {
33    pub fn contains(&self, header: &String) -> bool {
34        match self {
35            Self::All => false,
36            Self::Include(headers) => headers.contains(header),
37            Self::Exclude(headers) => !headers.contains(header),
38        }
39    }
40}
41
42/// MIME → MML message interpreter builder.
43#[derive(Clone, Debug, Default, Eq, PartialEq)]
44pub struct MimeInterpreterBuilder {
45    /// The strategy to display headers.
46    show_headers: FilterHeaders,
47
48    /// The internal MIME to MML message body interpreter.
49    mime_body_interpreter: MimeBodyInterpreter,
50}
51
52impl MimeInterpreterBuilder {
53    /// Create a new interpreter builder with default options.
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Filter headers with the given strategy.
59    pub fn with_show_headers(mut self, s: FilterHeaders) -> Self {
60        self.show_headers = s;
61        self
62    }
63
64    /// Show all headers.
65    pub fn with_show_all_headers(mut self) -> Self {
66        self.show_headers = FilterHeaders::All;
67        self
68    }
69
70    /// Show only headers matching the given ones.
71    pub fn with_show_only_headers(
72        mut self,
73        headers: impl IntoIterator<Item = impl ToString>,
74    ) -> Self {
75        let headers = headers.into_iter().fold(Vec::new(), |mut headers, header| {
76            let header = header.to_string();
77            if !headers.contains(&header) {
78                headers.push(header)
79            }
80            headers
81        });
82        self.show_headers = FilterHeaders::Include(headers);
83        self
84    }
85
86    /// Show additional headers.
87    // FIXME: seems not to work as expected, maybe need to use a
88    // different structure than [FilterHeaders].
89    pub fn with_show_additional_headers(
90        mut self,
91        headers: impl IntoIterator<Item = impl ToString>,
92    ) -> Self {
93        let next_headers = headers.into_iter().fold(Vec::new(), |mut headers, header| {
94            let header = header.to_string();
95            if !headers.contains(&header) && !self.show_headers.contains(&header) {
96                headers.push(header)
97            }
98            headers
99        });
100
101        match &mut self.show_headers {
102            FilterHeaders::All => {
103                // FIXME: this excludes all previous headers, needs to
104                // be separated.
105                self.show_headers = FilterHeaders::Include(next_headers);
106            }
107            FilterHeaders::Include(headers) => {
108                headers.extend(next_headers);
109            }
110            FilterHeaders::Exclude(headers) => {
111                headers.extend(next_headers);
112            }
113        };
114
115        self
116    }
117
118    /// Hide all headers.
119    pub fn with_hide_all_headers(mut self) -> Self {
120        self.show_headers = FilterHeaders::Include(Vec::new());
121        self
122    }
123
124    /// Show MML multipart tags.
125    pub fn with_show_multiparts(mut self, b: bool) -> Self {
126        self.mime_body_interpreter = self.mime_body_interpreter.with_show_multiparts(b);
127        self
128    }
129
130    /// Show MML parts tags.
131    pub fn with_show_parts(mut self, visibility: bool) -> Self {
132        self.mime_body_interpreter = self.mime_body_interpreter.with_show_parts(visibility);
133        self
134    }
135
136    /// Filter parts using the given strategy.
137    pub fn with_filter_parts(mut self, f: FilterParts) -> Self {
138        self.mime_body_interpreter = self.mime_body_interpreter.with_filter_parts(f);
139        self
140    }
141
142    /// Show plain texts signature.
143    pub fn with_show_plain_texts_signature(mut self, b: bool) -> Self {
144        self.mime_body_interpreter = self
145            .mime_body_interpreter
146            .with_show_plain_texts_signature(b);
147        self
148    }
149
150    /// Show MML attachments tags.
151    pub fn with_show_attachments(mut self, b: bool) -> Self {
152        self.mime_body_interpreter = self.mime_body_interpreter.with_show_attachments(b);
153        self
154    }
155
156    /// Show MML inline attachments tags.
157    pub fn with_show_inline_attachments(mut self, b: bool) -> Self {
158        self.mime_body_interpreter = self.mime_body_interpreter.with_show_inline_attachments(b);
159        self
160    }
161
162    /// Automatically save attachments to the `save_attachments_dir`.
163    pub fn with_save_attachments(mut self, b: bool) -> Self {
164        self.mime_body_interpreter = self.mime_body_interpreter.with_save_attachments(b);
165        self
166    }
167
168    /// Customize the download attachments directory.
169    ///
170    /// This can be used to display the `filename` property but also
171    /// to automatically save attachment with `save_attachments`.
172    pub fn with_save_attachments_dir(mut self, dir: impl Into<PathBuf>) -> Self {
173        self.mime_body_interpreter = self.mime_body_interpreter.with_save_attachments_dir(dir);
174        self
175    }
176
177    /// Customize the download attachments directory using an optional
178    /// path.
179    ///
180    /// This can be used to display the `filename` property but also
181    /// to automatically save attachment with `save_attachments`.
182    pub fn with_save_some_attachments_dir(self, dir: Option<impl Into<PathBuf>>) -> Self {
183        match dir {
184            Some(dir) => self.with_save_attachments_dir(dir),
185            None => {
186                self.with_save_attachments_dir(MimeBodyInterpreter::default_save_attachments_dir())
187            }
188        }
189    }
190
191    /// Customize PGP.
192    #[cfg(feature = "pgp")]
193    pub fn set_pgp(&mut self, pgp: impl Into<Pgp>) {
194        self.mime_body_interpreter.set_pgp(pgp);
195    }
196
197    /// Customize PGP.
198    #[cfg(feature = "pgp")]
199    pub fn with_pgp(mut self, pgp: impl Into<Pgp>) -> Self {
200        self.mime_body_interpreter.set_pgp(pgp);
201        self
202    }
203
204    /// Customize some PGP.
205    #[cfg(feature = "pgp")]
206    pub fn set_some_pgp(&mut self, pgp: Option<impl Into<Pgp>>) {
207        self.mime_body_interpreter.set_some_pgp(pgp);
208    }
209
210    /// Customize some PGP.
211    #[cfg(feature = "pgp")]
212    pub fn with_some_pgp(mut self, pgp: Option<impl Into<Pgp>>) -> Self {
213        self.mime_body_interpreter.set_some_pgp(pgp);
214        self
215    }
216
217    /// Build the final [MimeInterpreter].
218    ///
219    /// This intermediate step is not necessary for the interpreter,
220    /// the aim is just to have a common API with the compiler.
221    pub fn build(self) -> MimeInterpreter {
222        MimeInterpreter {
223            show_headers: self.show_headers,
224            mime_body_interpreter: self.mime_body_interpreter,
225        }
226    }
227}
228
229/// MIME → MML message interpreter.
230#[derive(Clone, Debug, Default, Eq, PartialEq)]
231pub struct MimeInterpreter {
232    show_headers: FilterHeaders,
233    mime_body_interpreter: MimeBodyInterpreter,
234}
235
236impl MimeInterpreter {
237    /// Interpret the given MIME [Message] as a MML [String].
238    pub async fn from_msg(self, msg: &Message<'_>) -> Result<String> {
239        let mut mml = String::new();
240
241        match self.show_headers {
242            FilterHeaders::All => msg.headers().iter().for_each(|header| {
243                let key = header.name.as_str();
244                let val = header::display_value(key, &header.value);
245                mml.push_str(&format!("{key}: {val}\n"));
246            }),
247            FilterHeaders::Include(keys) => keys
248                .iter()
249                .filter_map(|key| msg.header(key.as_str()).map(|val| (key, val)))
250                .for_each(|(key, val)| {
251                    let val = header::display_value(key, val);
252                    mml.push_str(&format!("{key}: {val}\n"));
253                }),
254            FilterHeaders::Exclude(keys) => msg
255                .headers()
256                .iter()
257                .filter(|header| !keys.contains(&header.name.as_str().to_owned()))
258                .for_each(|header| {
259                    let key = header.name.as_str();
260                    let val = header::display_value(key, &header.value);
261                    mml.push_str(&format!("{key}: {val}\n"));
262                }),
263        };
264
265        if !mml.is_empty() {
266            mml.push('\n');
267        }
268
269        let mime_body_interpreter = self.mime_body_interpreter;
270
271        #[cfg(feature = "pgp")]
272        let mime_body_interpreter = mime_body_interpreter
273            .with_pgp_sender(header::extract_first_email(msg.from()))
274            .with_pgp_recipient(header::extract_first_email(msg.to()));
275
276        let mml_body = mime_body_interpreter.interpret_msg(msg).await?;
277
278        mml.push_str(&mml_body);
279
280        Ok(mml)
281    }
282
283    /// Interpret the given MIME message bytes as a MML [String].
284    pub async fn from_bytes(self, bytes: impl AsRef<[u8]>) -> Result<String> {
285        let msg = MessageParser::new()
286            .parse(bytes.as_ref())
287            .ok_or(Error::ParseRawEmailError)?;
288        self.from_msg(&msg).await
289    }
290
291    /// Interpret the given MIME [MessageBuilder] as a MML [String].
292    pub async fn from_msg_builder(self, builder: MessageBuilder<'_>) -> Result<String> {
293        let bytes = builder.write_to_vec().map_err(Error::BuildEmailError)?;
294        self.from_bytes(&bytes).await
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use concat_with::concat_line;
301    use mail_builder::MessageBuilder;
302
303    use super::MimeInterpreterBuilder;
304
305    fn msg_builder() -> MessageBuilder<'static> {
306        MessageBuilder::new()
307            .message_id("id@localhost")
308            .in_reply_to("reply-id@localhost")
309            .date(0_u64)
310            .from("from@localhost")
311            .to("to@localhost")
312            .subject("subject")
313            .text_body("Hello, world!")
314    }
315
316    #[tokio::test]
317    async fn all_headers() {
318        let mml = MimeInterpreterBuilder::new()
319            .with_show_all_headers()
320            .build()
321            .from_msg_builder(msg_builder())
322            .await
323            .unwrap();
324
325        let expected_mml = concat_line!(
326            "Message-ID: <id@localhost>",
327            "In-Reply-To: <reply-id@localhost>",
328            "Date: Thu, 1 Jan 1970 00:00:00 +0000",
329            "From: from@localhost",
330            "To: to@localhost",
331            "Subject: subject",
332            "MIME-Version: 1.0",
333            "Content-Type: text/plain; charset=utf-8",
334            "Content-Transfer-Encoding: 7bit",
335            "",
336            "Hello, world!",
337        );
338
339        assert_eq!(mml, expected_mml);
340    }
341
342    #[tokio::test]
343    async fn only_headers() {
344        let mml = MimeInterpreterBuilder::new()
345            .with_show_only_headers(["From", "Subject"])
346            .build()
347            .from_msg_builder(msg_builder())
348            .await
349            .unwrap();
350
351        let expected_mml = concat_line!(
352            "From: from@localhost",
353            "Subject: subject",
354            "",
355            "Hello, world!",
356        );
357
358        assert_eq!(mml, expected_mml);
359    }
360
361    #[tokio::test]
362    async fn only_headers_duplicated() {
363        let mml = MimeInterpreterBuilder::new()
364            .with_show_only_headers(["From", "Subject", "From"])
365            .build()
366            .from_msg_builder(msg_builder())
367            .await
368            .unwrap();
369
370        let expected_mml = concat_line!(
371            "From: from@localhost",
372            "Subject: subject",
373            "",
374            "Hello, world!",
375        );
376
377        assert_eq!(mml, expected_mml);
378    }
379
380    #[tokio::test]
381    async fn no_headers() {
382        let mml = MimeInterpreterBuilder::new()
383            .with_hide_all_headers()
384            .build()
385            .from_msg_builder(msg_builder())
386            .await
387            .unwrap();
388
389        let expected_mml = concat_line!("Hello, world!");
390
391        assert_eq!(mml, expected_mml);
392    }
393
394    #[tokio::test]
395    async fn mml_markup_escaped() {
396        let msg_builder = MessageBuilder::new()
397            .message_id("id@localhost")
398            .in_reply_to("reply-id@localhost")
399            .date(0_u64)
400            .from("from@localhost")
401            .to("to@localhost")
402            .subject("subject")
403            .text_body("<#part>Should be escaped.<#/part>");
404
405        let mml = MimeInterpreterBuilder::new()
406            .with_show_only_headers(["From", "Subject"])
407            .build()
408            .from_msg_builder(msg_builder)
409            .await
410            .unwrap();
411
412        let expected_mml = concat_line!(
413            "From: from@localhost",
414            "Subject: subject",
415            "",
416            "<#!part>Should be escaped.<#!/part>",
417        );
418
419        assert_eq!(mml, expected_mml);
420    }
421}