mail_builder/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7#![doc = include_str!("../README.md")]
8#![deny(rust_2018_idioms)]
9#[forbid(unsafe_code)]
10pub mod encoders;
11pub mod headers;
12pub mod mime;
13
14use std::{
15    borrow::Cow,
16    io::{self, Write},
17};
18
19use headers::{
20    address::Address,
21    content_type::ContentType,
22    date::Date,
23    message_id::{generate_message_id_header, MessageId},
24    text::Text,
25    Header, HeaderType,
26};
27use mime::{BodyPart, MimePart};
28
29/// Builds an RFC5322 compliant MIME email message.
30#[derive(Clone, Debug)]
31pub struct MessageBuilder<'x> {
32    pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
33    pub html_body: Option<MimePart<'x>>,
34    pub text_body: Option<MimePart<'x>>,
35    pub attachments: Option<Vec<MimePart<'x>>>,
36    pub body: Option<MimePart<'x>>,
37}
38
39impl Default for MessageBuilder<'_> {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl<'x> MessageBuilder<'x> {
46    /// Create a new MessageBuilder.
47    pub fn new() -> Self {
48        MessageBuilder {
49            headers: Vec::new(),
50            html_body: None,
51            text_body: None,
52            attachments: None,
53            body: None,
54        }
55    }
56
57    /// Set the Message-ID header. If no Message-ID header is set, one will be
58    /// generated automatically.
59    pub fn message_id(self, value: impl Into<MessageId<'x>>) -> Self {
60        self.header("Message-ID", value.into())
61    }
62
63    /// Set the In-Reply-To header.
64    pub fn in_reply_to(self, value: impl Into<MessageId<'x>>) -> Self {
65        self.header("In-Reply-To", value.into())
66    }
67
68    /// Set the References header.
69    pub fn references(self, value: impl Into<MessageId<'x>>) -> Self {
70        self.header("References", value.into())
71    }
72
73    /// Set the Sender header.
74    pub fn sender(self, value: impl Into<Address<'x>>) -> Self {
75        self.header("Sender", value.into())
76    }
77
78    /// Set the From header.
79    pub fn from(self, value: impl Into<Address<'x>>) -> Self {
80        self.header("From", value.into())
81    }
82
83    /// Set the To header.
84    pub fn to(self, value: impl Into<Address<'x>>) -> Self {
85        self.header("To", value.into())
86    }
87
88    /// Set the Cc header.
89    pub fn cc(self, value: impl Into<Address<'x>>) -> Self {
90        self.header("Cc", value.into())
91    }
92
93    /// Set the Bcc header.
94    pub fn bcc(self, value: impl Into<Address<'x>>) -> Self {
95        self.header("Bcc", value.into())
96    }
97
98    /// Set the Reply-To header.
99    pub fn reply_to(self, value: impl Into<Address<'x>>) -> Self {
100        self.header("Reply-To", value.into())
101    }
102
103    /// Set the Subject header.
104    pub fn subject(self, value: impl Into<Text<'x>>) -> Self {
105        self.header("Subject", value.into())
106    }
107
108    /// Set the Date header. If no Date header is set, one will be generated
109    /// automatically.
110    pub fn date(self, value: impl Into<Date>) -> Self {
111        self.header("Date", value.into())
112    }
113
114    /// Add a custom header.
115    pub fn header(
116        mut self,
117        header: impl Into<Cow<'x, str>>,
118        value: impl Into<HeaderType<'x>>,
119    ) -> Self {
120        self.headers.push((header.into(), value.into()));
121        self
122    }
123
124    /// Set custom headers.
125    pub fn headers<T, U, V>(mut self, header: T, values: U) -> Self
126    where
127        T: Into<Cow<'x, str>>,
128        U: IntoIterator<Item = V>,
129        V: Into<HeaderType<'x>>,
130    {
131        let header = header.into();
132
133        for value in values {
134            self.headers.push((header.clone(), value.into()));
135        }
136
137        self
138    }
139
140    /// Set the plain text body of the message. Note that only one plain text body
141    /// per message can be set using this function.
142    /// To build more complex MIME body structures, use the `body` method instead.
143    pub fn text_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
144        self.text_body = Some(MimePart::new("text/plain", BodyPart::Text(value.into())));
145        self
146    }
147
148    /// Set the HTML body of the message. Note that only one HTML body
149    /// per message can be set using this function.
150    /// To build more complex MIME body structures, use the `body` method instead.
151    pub fn html_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
152        self.html_body = Some(MimePart::new("text/html", BodyPart::Text(value.into())));
153        self
154    }
155
156    /// Add a binary attachment to the message.
157    pub fn attachment(
158        mut self,
159        content_type: impl Into<ContentType<'x>>,
160        filename: impl Into<Cow<'x, str>>,
161        value: impl Into<BodyPart<'x>>,
162    ) -> Self {
163        self.attachments
164            .get_or_insert_with(Vec::new)
165            .push(MimePart::new(content_type, value).attachment(filename));
166        self
167    }
168
169    /// Add an inline binary to the message.
170    pub fn inline(
171        mut self,
172        content_type: impl Into<ContentType<'x>>,
173        cid: impl Into<Cow<'x, str>>,
174        value: impl Into<BodyPart<'x>>,
175    ) -> Self {
176        self.attachments
177            .get_or_insert_with(Vec::new)
178            .push(MimePart::new(content_type, value).inline().cid(cid));
179        self
180    }
181
182    /// Set a custom MIME body structure.
183    pub fn body(mut self, value: MimePart<'x>) -> Self {
184        self.body = Some(value);
185        self
186    }
187
188    /// Build the message.
189    pub fn write_to(self, mut output: impl Write) -> io::Result<()> {
190        let mut has_date = false;
191        let mut has_message_id = false;
192        let mut has_mime_version = false;
193
194        for (header_name, header_value) in &self.headers {
195            if !has_date && header_name == "Date" {
196                has_date = true;
197            } else if !has_message_id && header_name == "Message-ID" {
198                has_message_id = true;
199            } else if !has_mime_version && header_name == "MIME-Version" {
200                has_mime_version = true;
201            }
202
203            output.write_all(header_name.as_bytes())?;
204            output.write_all(b": ")?;
205            header_value.write_header(&mut output, header_name.len() + 2)?;
206        }
207
208        if !has_message_id {
209            output.write_all(b"Message-ID: ")?;
210
211            #[cfg(feature = "gethostname")]
212            generate_message_id_header(
213                &mut output,
214                gethostname::gethostname().to_str().unwrap_or("localhost"),
215            )?;
216
217            #[cfg(not(feature = "gethostname"))]
218            generate_message_id_header(&mut output, "localhost")?;
219
220            output.write_all(b"\r\n")?;
221        }
222
223        if !has_date {
224            output.write_all(b"Date: ")?;
225            output.write_all(Date::now().to_rfc822().as_bytes())?;
226            output.write_all(b"\r\n")?;
227        }
228
229        if !has_mime_version {
230            output.write_all(b"MIME-Version: 1.0\r\n")?;
231        }
232
233        self.write_body(output)
234    }
235
236    /// Write the message body without headers.
237    pub fn write_body(self, output: impl Write) -> io::Result<()> {
238        (if let Some(body) = self.body {
239            body
240        } else {
241            match (self.text_body, self.html_body, self.attachments) {
242                (Some(text), Some(html), Some(attachments)) => {
243                    let mut parts = Vec::with_capacity(attachments.len() + 1);
244                    parts.push(MimePart::new("multipart/alternative", vec![text, html]));
245                    parts.extend(attachments);
246
247                    MimePart::new("multipart/mixed", parts)
248                }
249                (Some(text), Some(html), None) => {
250                    MimePart::new("multipart/alternative", vec![text, html])
251                }
252                (Some(text), None, Some(attachments)) => {
253                    let mut parts = Vec::with_capacity(attachments.len() + 1);
254                    parts.push(text);
255                    parts.extend(attachments);
256                    MimePart::new("multipart/mixed", parts)
257                }
258                (Some(text), None, None) => text,
259                (None, Some(html), Some(attachments)) => {
260                    let mut parts = Vec::with_capacity(attachments.len() + 1);
261                    parts.push(html);
262                    parts.extend(attachments);
263                    MimePart::new("multipart/mixed", parts)
264                }
265                (None, Some(html), None) => html,
266                (None, None, Some(attachments)) => MimePart::new("multipart/mixed", attachments),
267                (None, None, None) => MimePart::new("text/plain", "\n"),
268            }
269        })
270        .write_part(output)?;
271
272        Ok(())
273    }
274
275    /// Build message to a Vec<u8>.
276    pub fn write_to_vec(self) -> io::Result<Vec<u8>> {
277        let mut output = Vec::new();
278        self.write_to(&mut output)?;
279        Ok(output)
280    }
281
282    /// Build message to a String.
283    pub fn write_to_string(self) -> io::Result<String> {
284        let mut output = Vec::new();
285        self.write_to(&mut output)?;
286        String::from_utf8(output).map_err(io::Error::other)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292
293    use mail_parser::MessageParser;
294
295    use crate::{
296        headers::{address::Address, url::URL},
297        mime::MimePart,
298        MessageBuilder,
299    };
300
301    #[test]
302    fn build_nested_message() {
303        let output = MessageBuilder::new()
304            .from(Address::new_address("John Doe".into(), "john@doe.com"))
305            .to(Address::new_address("Jane Doe".into(), "jane@doe.com"))
306            .subject("RFC 8621 Section 4.1.4 test")
307            .body(MimePart::new(
308                "multipart/mixed",
309                vec![
310                    MimePart::new("text/plain", "Part A contents go here...").inline(),
311                    MimePart::new(
312                        "multipart/mixed",
313                        vec![
314                            MimePart::new(
315                                "multipart/alternative",
316                                vec![
317                                    MimePart::new(
318                                        "multipart/mixed",
319                                        vec![
320                                            MimePart::new(
321                                                "text/plain",
322                                                "Part B contents go here...",
323                                            )
324                                            .inline(),
325                                            MimePart::new(
326                                                "image/jpeg",
327                                                "Part C contents go here...".as_bytes(),
328                                            )
329                                            .inline(),
330                                            MimePart::new(
331                                                "text/plain",
332                                                "Part D contents go here...",
333                                            )
334                                            .inline(),
335                                        ],
336                                    ),
337                                    MimePart::new(
338                                        "multipart/related",
339                                        vec![
340                                            MimePart::new(
341                                                "text/html",
342                                                "Part E contents go here...",
343                                            )
344                                            .inline(),
345                                            MimePart::new(
346                                                "image/jpeg",
347                                                "Part F contents go here...".as_bytes(),
348                                            ),
349                                        ],
350                                    ),
351                                ],
352                            ),
353                            MimePart::new("image/jpeg", "Part G contents go here...".as_bytes())
354                                .attachment("image_G.jpg"),
355                            MimePart::new(
356                                "application/x-excel",
357                                "Part H contents go here...".as_bytes(),
358                            ),
359                            MimePart::new(
360                                "x-message/rfc822",
361                                "Part J contents go here...".as_bytes(),
362                            ),
363                        ],
364                    ),
365                    MimePart::new("text/plain", "Part K contents go here...").inline(),
366                ],
367            ))
368            .write_to_vec()
369            .unwrap();
370        MessageParser::new().parse(&output).unwrap();
371        //fs::write("test.yaml", &serde_yaml::to_string(&message).unwrap()).unwrap();
372    }
373
374    #[test]
375    fn build_message() {
376        let output = MessageBuilder::new()
377            .from(("John Doe", "john@doe.com"))
378            .to(vec![
379                ("Antoine de Saint-Exupéry", "antoine@exupery.com"),
380                ("안녕하세요 세계", "test@test.com"),
381                ("Xin chào", "addr@addr.com"),
382            ])
383            .bcc(vec![
384                (
385                    "Привет, мир",
386                    vec![
387                        ("ASCII recipient", "addr1@addr7.com"),
388                        ("ハロー・ワールド", "addr2@addr6.com"),
389                        ("áéíóú", "addr3@addr5.com"),
390                        ("Γειά σου Κόσμε", "addr4@addr4.com"),
391                    ],
392                ),
393                (
394                    "Hello world",
395                    vec![
396                        ("שלום עולם", "addr5@addr3.com"),
397                        ("¡El ñandú comió ñoquis!", "addr6@addr2.com"),
398                        ("Recipient", "addr7@addr1.com"),
399                    ],
400                ),
401            ])
402            .header("List-Archive", URL::new("http://example.com/archive"))
403            .subject("Hello world!")
404            .text_body("Hello, world!\n".repeat(20))
405            .html_body("<p>¡Hola Mundo!</p>".repeat(20))
406            .inline("image/png", "cid:image", [0, 1, 2, 3, 4, 5].as_ref())
407            .attachment("text/plain", "my fíle.txt", "안녕하세요 세계".repeat(20))
408            .attachment(
409                "text/plain",
410                "ハロー・ワールド",
411                "ハロー・ワールド".repeat(20).into_bytes(),
412            )
413            .write_to_vec()
414            .unwrap();
415        MessageParser::new().parse(&output).unwrap();
416    }
417}