mml/message/
compiler.rs

1//! # MML to MIME message compilation module
2//!
3//! Module dedicated to MML → MIME message compilation.
4
5use mail_builder::{headers::text::Text, MessageBuilder};
6use mail_parser::{Message, MessageParser};
7
8#[cfg(feature = "pgp")]
9use crate::{message::header, pgp::Pgp};
10use crate::{message::MmlBodyCompiler, Error, Result};
11
12/// MML → MIME message compiler builder.
13///
14/// The compiler follows the builder pattern, where the build function
15/// is named `compile`.
16#[derive(Clone, Debug, Default)]
17pub struct MmlCompilerBuilder {
18    /// The internal MML to MIME message body compiler.
19    mml_body_compiler: MmlBodyCompiler,
20}
21
22impl MmlCompilerBuilder {
23    /// Create a new compiler builder with default options.
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Customize PGP.
29    #[cfg(feature = "pgp")]
30    pub fn set_pgp(&mut self, pgp: impl Into<Pgp>) {
31        self.mml_body_compiler.set_pgp(pgp);
32    }
33
34    /// Customize PGP.
35    #[cfg(feature = "pgp")]
36    pub fn with_pgp(mut self, pgp: impl Into<Pgp>) -> Self {
37        self.mml_body_compiler.set_pgp(pgp);
38        self
39    }
40
41    /// Customize some PGP.
42    #[cfg(feature = "pgp")]
43    pub fn set_some_pgp(&mut self, pgp: Option<impl Into<Pgp>>) {
44        self.mml_body_compiler.set_some_pgp(pgp);
45    }
46
47    /// Customize some PGP.
48    #[cfg(feature = "pgp")]
49    pub fn with_some_pgp(mut self, pgp: Option<impl Into<Pgp>>) -> Self {
50        self.mml_body_compiler.set_some_pgp(pgp);
51        self
52    }
53
54    /// Build the final [MmlCompiler] based on the defined options.
55    pub fn build(self, mml_msg: &str) -> Result<MmlCompiler<'_>> {
56        let mml_msg = MessageParser::new()
57            .parse(mml_msg.as_bytes())
58            .ok_or(Error::ParseMessageError)?;
59        let mml_body_compiler = self.mml_body_compiler;
60
61        #[cfg(feature = "pgp")]
62        let mml_body_compiler = mml_body_compiler
63            .with_pgp_recipients(header::extract_emails(mml_msg.to()))
64            .with_pgp_sender(header::extract_first_email(mml_msg.from()));
65
66        Ok(MmlCompiler {
67            mml_msg,
68            mml_body_compiler,
69        })
70    }
71}
72
73/// MML → MIME message compiler.
74///
75/// This structure allows users to choose the final form of the
76/// desired MIME message: [MessageBuilder], [Vec], [String] etc.
77#[derive(Clone, Debug, Default)]
78pub struct MmlCompiler<'a> {
79    mml_msg: Message<'a>,
80    mml_body_compiler: MmlBodyCompiler,
81}
82
83impl MmlCompiler<'_> {
84    /// Compile the inner MML message into a [MmlCompileResult].
85    ///
86    /// The fact to return a intermediate structure allows users to
87    /// customize the final form of the desired MIME message.
88    pub async fn compile(&self) -> Result<MmlCompileResult<'_>> {
89        let mml_body = self
90            .mml_msg
91            .text_bodies()
92            .next()
93            .ok_or(Error::ParseMmlEmptyBodyError)?
94            .text_contents()
95            .ok_or(Error::ParseMmlEmptyBodyContentError)?;
96
97        let mml_body_compiler = &self.mml_body_compiler;
98
99        let mut mime_msg_builder = mml_body_compiler.compile(mml_body).await?;
100
101        mime_msg_builder = mime_msg_builder.header("MIME-Version", Text::new("1.0"));
102
103        for header in self.mml_msg.headers() {
104            let key = header.name.as_str();
105            let val = super::header::to_builder_val(header);
106            mime_msg_builder = mime_msg_builder.header(key, val);
107        }
108
109        Ok(MmlCompileResult { mime_msg_builder })
110    }
111}
112
113/// MML → MIME message compilation result.
114///
115/// This structure allows users to choose the final form of the
116/// desired MIME message: [MessageBuilder], [Vec], [String] etc.
117#[derive(Clone, Debug, Default)]
118pub struct MmlCompileResult<'a> {
119    mime_msg_builder: MessageBuilder<'a>,
120}
121
122impl<'a> MmlCompileResult<'a> {
123    /// Return a reference to the final MIME message builder.
124    pub fn as_msg_builder(&self) -> &MessageBuilder {
125        &self.mime_msg_builder
126    }
127
128    /// Return a copy of the final MIME message builder.
129    pub fn to_msg_builder(&self) -> MessageBuilder {
130        self.mime_msg_builder.clone()
131    }
132
133    /// Return the final MIME message builder.
134    pub fn into_msg_builder(self) -> MessageBuilder<'a> {
135        self.mime_msg_builder
136    }
137
138    /// Return the final MIME message as a [Vec].
139    pub fn into_vec(self) -> Result<Vec<u8>> {
140        self.mime_msg_builder
141            .write_to_vec()
142            .map_err(Error::CompileMmlMessageToVecError)
143    }
144
145    /// Return the final MIME message as a [String].
146    pub fn into_string(self) -> Result<String> {
147        self.mime_msg_builder
148            .write_to_string()
149            .map_err(Error::CompileMmlMessageToStringError)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use concat_with::concat_line;
156
157    use crate::{MimeInterpreterBuilder, MmlCompilerBuilder};
158
159    #[tokio::test]
160    async fn non_ascii_headers() {
161        let mml = concat_line!(
162            "Message-ID: <id@localhost>",
163            "Date: Thu, 1 Jan 1970 00:00:00 +0000",
164            "From: Frȯm <from@localhost>",
165            "To: Tó <to@localhost>",
166            "Subject: Subjêct",
167            "",
168            "Hello, world!",
169            "",
170        );
171
172        let mml_compiler = MmlCompilerBuilder::new().build(mml).unwrap();
173        let mime_msg_builder = mml_compiler.compile().await.unwrap().into_msg_builder();
174
175        let mml_msg = MimeInterpreterBuilder::new()
176            .with_show_only_headers(["From", "To", "Subject"])
177            .build()
178            .from_msg_builder(mime_msg_builder)
179            .await
180            .unwrap();
181
182        let expected_mml_msg = concat_line!(
183            "From: Frȯm <from@localhost>",
184            "To: Tó <to@localhost>",
185            "Subject: Subjêct",
186            "",
187            "Hello, world!",
188            "",
189        );
190
191        assert_eq!(mml_msg, expected_mml_msg);
192    }
193
194    #[tokio::test]
195    async fn message_id_with_angles() {
196        let mml = concat_line!(
197            "From: Hugo Osvaldo Barrera <hugo@localhost>",
198            "To: Hugo Osvaldo Barrera <hugo@localhost>",
199            "Cc:",
200            "Subject: Blah",
201            "Message-ID: <bfb64e12-b7d4-474c-a658-8a221365f8ca@localhost>",
202            "",
203            "Test message",
204            "",
205        );
206
207        let mml_compiler = MmlCompilerBuilder::new().build(mml).unwrap();
208        let mime_msg_builder = mml_compiler.compile().await.unwrap().into_msg_builder();
209
210        let mml_msg = MimeInterpreterBuilder::new()
211            .with_show_only_headers(["Message-ID"])
212            .build()
213            .from_msg_builder(mime_msg_builder)
214            .await
215            .unwrap();
216
217        let expected_mml_msg = concat_line!(
218            "Message-ID: <bfb64e12-b7d4-474c-a658-8a221365f8ca@localhost>",
219            "",
220            "Test message",
221            "",
222        );
223
224        assert_eq!(mml_msg, expected_mml_msg);
225    }
226
227    #[tokio::test]
228    async fn message_id_without_angles() {
229        let mml = concat_line!(
230            "From: Hugo Osvaldo Barrera <hugo@localhost>",
231            "To: Hugo Osvaldo Barrera <hugo@localhost>",
232            "Cc:",
233            "Subject: Blah",
234            "Message-ID: bfb64e12-b7d4-474c-a658-8a221365f8ca@localhost",
235            "",
236            "Test message",
237            "",
238        );
239
240        let mml_compiler = MmlCompilerBuilder::new().build(mml).unwrap();
241        let mime_msg_builder = mml_compiler.compile().await.unwrap().into_msg_builder();
242
243        let mml_msg = MimeInterpreterBuilder::new()
244            .with_show_only_headers(["Message-ID"])
245            .build()
246            .from_msg_builder(mime_msg_builder)
247            .await
248            .unwrap();
249
250        let expected_mml_msg = concat_line!(
251            "Message-ID: <bfb64e12-b7d4-474c-a658-8a221365f8ca@localhost>",
252            "",
253            "Test message",
254            "",
255        );
256
257        assert_eq!(mml_msg, expected_mml_msg);
258    }
259
260    #[tokio::test]
261    async fn mml_markup_unescaped() {
262        let mml = concat_line!(
263            "Message-ID: <id@localhost>",
264            "Date: Thu, 1 Jan 1970 00:00:00 +0000",
265            "From: from@localhost",
266            "To: to@localhost",
267            "Subject: subject",
268            "",
269            "<#!part>This should be unescaped<#!/part>",
270            "",
271        );
272
273        let mml_compiler = MmlCompilerBuilder::new().build(mml).unwrap();
274        let compile_mml_res = mml_compiler.compile().await.unwrap();
275        let mime_msg_builder = compile_mml_res.clone().into_msg_builder();
276        let mime_msg_str = compile_mml_res.into_string().unwrap();
277
278        let mml_msg = MimeInterpreterBuilder::new()
279            .with_show_only_headers(["From", "To", "Subject"])
280            .build()
281            .from_msg_builder(mime_msg_builder)
282            .await
283            .unwrap();
284
285        let expected_mml_msg = concat_line!(
286            "From: from@localhost",
287            "To: to@localhost",
288            "Subject: subject",
289            "",
290            "<#!part>This should be unescaped<#!/part>",
291            "",
292        );
293
294        assert!(!mime_msg_str.contains("<#!part>"));
295        assert!(mime_msg_str.contains("<#part>"));
296
297        assert!(!mime_msg_str.contains("<#!/part>"));
298        assert!(mime_msg_str.contains("<#/part>"));
299
300        assert_eq!(mml_msg, expected_mml_msg);
301    }
302}