Skip to main content

mail_builder/
lib.rs

1/*
2 * Copyright Stalwart Labs Ltd. See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12//! # mail-builder
13//!
14//! [![crates.io](https://img.shields.io/crates/v/mail-builder)](https://crates.io/crates/mail-builder)
15//! [![build](https://github.com/stalwartlabs/mail-builder/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/mail-builder/actions/workflows/rust.yml)
16//! [![docs.rs](https://img.shields.io/docsrs/mail-builder)](https://docs.rs/mail-builder)
17//! [![crates.io](https://img.shields.io/crates/l/mail-builder)](http://www.apache.org/licenses/LICENSE-2.0)
18//!
19//! _mail-builder_ is a flexible **e-mail builder library** written in Rust. It includes the following features:
20//!
21//! - Generates **e-mail** messages conforming to the Internet Message Format standard (_RFC 5322_).
22//! - Full **MIME** support (_RFC 2045 - 2049_) with automatic selection of the most optimal encoding for each message body part.
23//! - **Fast Base64 encoding** based on Chromium's decoder ([the fastest non-SIMD encoder](https://github.com/lemire/fastbase64)).
24//! - Minimal dependencies.
25//!
26//! Please note that this library does not support sending or parsing e-mail messages as these functionalities are provided by the crates [`mail-send`](https://crates.io/crates/mail-send) and [`mail-parser`](https://crates.io/crates/mail-parser).
27//!
28//! ## Usage Example
29//!
30//! Build a simple e-mail message with a text body and one attachment:
31//!
32//! ```rust
33//!     use mail_builder::MessageBuilder;
34//!
35//!    // Build a simple text message with a single attachment
36//!    let eml = MessageBuilder::new()
37//!        .from(("John Doe", "john@doe.com"))
38//!        .to("jane@doe.com")
39//!        .subject("Hello, world!")
40//!        .text_body("Message contents go here.")
41//!        .attachment("image/png", "image.png", [1, 2, 3, 4].as_ref())
42//!        .write_to_string()
43//!        .unwrap();
44//!
45//!    // Print raw message
46//!    println!("{}", eml);
47//! ```
48//!
49//! More complex messages with grouped addresses, inline parts and
50//! multipart/alternative sections can also be easily built:
51//!
52//! ```rust
53//!     use mail_builder::{headers::url::URL, MessageBuilder};
54//!     use std::fs::File;
55//!
56//!    // Build a multipart message with text and HTML bodies,
57//!    // inline parts and attachments.
58//!    MessageBuilder::new()
59//!        .from(("John Doe", "john@doe.com"))
60//!
61//!        // To recipients
62//!        .to(vec![
63//!            ("Antoine de Saint-Exupéry", "antoine@exupery.com"),
64//!            ("안녕하세요 세계", "test@test.com"),
65//!            ("Xin chào", "addr@addr.com"),
66//!        ])
67//!
68//!        // BCC recipients using grouped addresses
69//!        .bcc(vec![
70//!            (
71//!                "My Group",
72//!                vec![
73//!                    ("ASCII name", "addr1@addr7.com"),
74//!                    ("ハロー・ワールド", "addr2@addr6.com"),
75//!                    ("áéíóú", "addr3@addr5.com"),
76//!                    ("Γειά σου Κόσμε", "addr4@addr4.com"),
77//!                ],
78//!            ),
79//!            (
80//!                "Another Group",
81//!                vec![
82//!                    ("שלום עולם", "addr5@addr3.com"),
83//!                    ("ñandú come ñoquis", "addr6@addr2.com"),
84//!                    ("Recipient", "addr7@addr1.com"),
85//!                ],
86//!            ),
87//!        ])
88//!
89//!        // Set RFC and custom headers
90//!        .subject("Testing multipart messages")
91//!        .in_reply_to(vec!["message-id-1", "message-id-2"])
92//!        .header("List-Archive", URL::new("http://example.com/archive"))
93//!
94//!        // Set HTML and plain text bodies
95//!        .text_body("This is the text body!\n")
96//!        .html_body("<p>HTML body with <img src=\"cid:my-image\"/>!</p>")
97//!
98//!        // Include an embedded image as an inline part
99//!        .inline("image/png", "cid:my-image", [0, 1, 2, 3, 4, 5].as_ref())
100//!        .attachment("text/plain", "my fíle.txt", "Attachment contents go here.")
101//!
102//!        // Add text and binary attachments
103//!        .attachment(
104//!            "text/plain",
105//!            "ハロー・ワールド",
106//!            b"Binary contents go here.".as_ref(),
107//!        )
108//!
109//!        // Write the message to a file
110//!        .write_to(File::create("message.eml").unwrap())
111//!        .unwrap();
112//! ```
113//!
114//! Nested MIME body structures can be created using the `body` method:
115//!
116//! ```rust
117//!     use mail_builder::{headers::address::Address, mime::MimePart, MessageBuilder};
118//!     use std::fs::File;
119//!
120//!    // Build a nested multipart message
121//!    MessageBuilder::new()
122//!        .from(Address::new_address("John Doe".into(), "john@doe.com"))
123//!        .to(Address::new_address("Jane Doe".into(), "jane@doe.com"))
124//!        .subject("Nested multipart message")
125//!
126//!        // Define the nested MIME body structure
127//!        .body(MimePart::new(
128//!            "multipart/mixed",
129//!            vec![
130//!                MimePart::new("text/plain", "Part A contents go here...").inline(),
131//!                MimePart::new(
132//!                    "multipart/mixed",
133//!                    vec![
134//!                        MimePart::new(
135//!                            "multipart/alternative",
136//!                            vec![
137//!                                MimePart::new(
138//!                                    "multipart/mixed",
139//!                                    vec![
140//!                                        MimePart::new("text/plain", "Part B contents go here...").inline(),
141//!                                        MimePart::new(
142//!                                            "image/jpeg",
143//!                                            "Part C contents go here...".as_bytes(),
144//!                                        )
145//!                                        .inline(),
146//!                                        MimePart::new("text/plain", "Part D contents go here...").inline(),
147//!                                    ],
148//!                                ),
149//!                                MimePart::new(
150//!                                    "multipart/related",
151//!                                    vec![
152//!                                        MimePart::new("text/html", "Part E contents go here...").inline(),
153//!                                        MimePart::new(
154//!                                            "image/jpeg",
155//!                                            "Part F contents go here...".as_bytes(),
156//!                                        ),
157//!                                    ],
158//!                                ),
159//!                            ],
160//!                        ),
161//!                        MimePart::new("image/jpeg", "Part G contents go here...".as_bytes())
162//!                            .attachment("image_G.jpg"),
163//!                        MimePart::new(
164//!                            "application/x-excel",
165//!                            "Part H contents go here...".as_bytes(),
166//!                        ),
167//!                        MimePart::new(
168//!                            "x-message/rfc822",
169//!                            "Part J contents go here...".as_bytes(),
170//!                        ),
171//!                    ],
172//!                ),
173//!                MimePart::new("text/plain", "Part K contents go here...").inline(),
174//!            ],
175//!        ))
176//!
177//!        // Write the message to a file
178//!        .write_to(File::create("nested-message.eml").unwrap())
179//!        .unwrap();
180//! ```
181//!
182//! ## Testing
183//!
184//! To run the testsuite:
185//!
186//! ```bash
187//!  $ cargo test --all-features
188//! ```
189//!
190//! or, to run the testsuite with MIRI:
191//!
192//! ```bash
193//!  $ cargo +nightly miri test --all-features
194//! ```
195//!
196//! ## License
197//!
198//! Licensed under either of
199//!
200//!  * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
201//!  * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
202//!
203//! at your option.
204//!
205//! ## Copyright
206//!
207//! Copyright (C) 2020-2022, Stalwart Labs Ltd.
208//!
209//! See [COPYING] for the license.
210//!
211//! [COPYING]: https://github.com/stalwartlabs/mail-builder/blob/main/COPYING
212//!
213pub mod encoders;
214pub mod headers;
215pub mod mime;
216
217use std::{
218    borrow::Cow,
219    io::{self, Write},
220};
221
222use headers::{
223    Header, HeaderType,
224    address::Address,
225    content_type::ContentType,
226    date::Date,
227    message_id::{MessageId, generate_message_id_header},
228    text::Text,
229};
230use mime::{BodyPart, MimePart};
231
232/// Builds an RFC5322 compliant MIME email message.
233#[derive(Clone, Debug)]
234pub struct MessageBuilder<'x> {
235    pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
236    pub html_body: Option<MimePart<'x>>,
237    pub text_body: Option<MimePart<'x>>,
238    pub attachments: Option<Vec<MimePart<'x>>>,
239    pub body: Option<MimePart<'x>>,
240}
241
242impl<'x> Default for MessageBuilder<'x> {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248impl<'x> MessageBuilder<'x> {
249    /// Create a new MessageBuilder.
250    pub fn new() -> Self {
251        MessageBuilder {
252            headers: Vec::new(),
253            html_body: None,
254            text_body: None,
255            attachments: None,
256            body: None,
257        }
258    }
259
260    /// Set the Message-ID header. If no Message-ID header is set, one will be
261    /// generated automatically.
262    pub fn message_id(self, value: impl Into<MessageId<'x>>) -> Self {
263        self.header("Message-ID", value.into())
264    }
265
266    /// Set the In-Reply-To header.
267    pub fn in_reply_to(self, value: impl Into<MessageId<'x>>) -> Self {
268        self.header("In-Reply-To", value.into())
269    }
270
271    /// Set the References header.
272    pub fn references(self, value: impl Into<MessageId<'x>>) -> Self {
273        self.header("References", value.into())
274    }
275
276    /// Set the Sender header.
277    pub fn sender(self, value: impl Into<Address<'x>>) -> Self {
278        self.header("Sender", value.into())
279    }
280
281    /// Set the From header.
282    pub fn from(self, value: impl Into<Address<'x>>) -> Self {
283        self.header("From", value.into())
284    }
285
286    /// Set the To header.
287    pub fn to(self, value: impl Into<Address<'x>>) -> Self {
288        self.header("To", value.into())
289    }
290
291    /// Set the Cc header.
292    pub fn cc(self, value: impl Into<Address<'x>>) -> Self {
293        self.header("Cc", value.into())
294    }
295
296    /// Set the Bcc header.
297    pub fn bcc(self, value: impl Into<Address<'x>>) -> Self {
298        self.header("Bcc", value.into())
299    }
300
301    /// Set the Reply-To header.
302    pub fn reply_to(self, value: impl Into<Address<'x>>) -> Self {
303        self.header("Reply-To", value.into())
304    }
305
306    /// Set the Subject header.
307    pub fn subject(self, value: impl Into<Text<'x>>) -> Self {
308        self.header("Subject", value.into())
309    }
310
311    /// Set the Date header. If no Date header is set, one will be generated
312    /// automatically.
313    pub fn date(self, value: impl Into<Date>) -> Self {
314        self.header("Date", value.into())
315    }
316
317    /// Add a custom header.
318    pub fn header(mut self, header: impl Into<Cow<'x, str>>, value: impl Into<HeaderType<'x>>) -> Self {
319        self.headers.push((header.into(), value.into()));
320        self
321    }
322
323    /// Set custom headers.
324    pub fn headers<T, U, V>(mut self, header: T, values: U) -> Self
325    where
326        T: Into<Cow<'x, str>>,
327        U: IntoIterator<Item = V>,
328        V: Into<HeaderType<'x>>,
329    {
330        let header = header.into();
331
332        for value in values {
333            self.headers.push((header.clone(), value.into()));
334        }
335
336        self
337    }
338
339    /// Set the plain text body of the message. Note that only one plain text body
340    /// per message can be set using this function.
341    /// To build more complex MIME body structures, use the `body` method instead.
342    pub fn text_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
343        self.text_body = Some(MimePart::new("text/plain", BodyPart::Text(value.into())));
344        self
345    }
346
347    /// Set the HTML body of the message. Note that only one HTML body
348    /// per message can be set using this function.
349    /// To build more complex MIME body structures, use the `body` method instead.
350    pub fn html_body(mut self, value: impl Into<Cow<'x, str>>) -> Self {
351        self.html_body = Some(MimePart::new("text/html", BodyPart::Text(value.into())));
352        self
353    }
354
355    /// Add a binary attachment to the message.
356    pub fn attachment(
357        mut self,
358        content_type: impl Into<ContentType<'x>>,
359        filename: impl Into<Cow<'x, str>>,
360        value: impl Into<BodyPart<'x>>,
361    ) -> Self {
362        self.attachments
363            .get_or_insert_with(Vec::new)
364            .push(MimePart::new(content_type, value).attachment(filename));
365        self
366    }
367
368    /// Add an inline binary to the message.
369    pub fn inline(
370        mut self,
371        content_type: impl Into<ContentType<'x>>,
372        cid: impl Into<Cow<'x, str>>,
373        value: impl Into<BodyPart<'x>>,
374    ) -> Self {
375        self.attachments
376            .get_or_insert_with(Vec::new)
377            .push(MimePart::new(content_type, value).inline().cid(cid));
378        self
379    }
380
381    /// Set a custom MIME body structure.
382    pub fn body(mut self, value: MimePart<'x>) -> Self {
383        self.body = Some(value);
384        self
385    }
386
387    /// Build the message.
388    pub fn write_to(self, mut output: impl Write) -> io::Result<()> {
389        let mut has_date = false;
390        let mut has_message_id = false;
391        let mut has_mime_version = false;
392
393        for (header_name, header_value) in &self.headers {
394            if !has_date && header_name == "Date" {
395                has_date = true;
396            } else if !has_message_id && header_name == "Message-ID" {
397                has_message_id = true;
398            } else if !has_mime_version && header_name == "MIME-Version" {
399                has_mime_version = true;
400            }
401
402            output.write_all(header_name.as_bytes())?;
403            output.write_all(b": ")?;
404            header_value.write_header(&mut output, header_name.len() + 2)?;
405        }
406
407        if !has_message_id {
408            output.write_all(b"Message-ID: ")?;
409            // PATCH
410            // let hostname = gethostname::gethostname().to_str().unwrap_or("localhost");
411            generate_message_id_header(&mut output, "localhost.local")?;
412            output.write_all(b"\r\n")?;
413        }
414
415        if !has_date {
416            output.write_all(b"Date: ")?;
417            output.write_all(Date::now().to_rfc822().as_bytes())?;
418            output.write_all(b"\r\n")?;
419        }
420
421        if !has_mime_version {
422            output.write_all(b"MIME-Version: 1.0\r\n")?;
423        }
424
425        self.write_body(output)
426    }
427
428    /// Write the message body without headers.
429    pub fn write_body(self, output: impl Write) -> io::Result<()> {
430        (if let Some(body) = self.body {
431            body
432        } else {
433            match (self.text_body, self.html_body, self.attachments) {
434                (Some(text), Some(html), Some(attachments)) => {
435                    let mut parts = Vec::with_capacity(attachments.len() + 1);
436                    parts.push(MimePart::new("multipart/alternative", vec![text, html]));
437                    parts.extend(attachments);
438
439                    MimePart::new("multipart/mixed", parts)
440                }
441                (Some(text), Some(html), None) => MimePart::new("multipart/alternative", vec![text, html]),
442                (Some(text), None, Some(attachments)) => {
443                    let mut parts = Vec::with_capacity(attachments.len() + 1);
444                    parts.push(text);
445                    parts.extend(attachments);
446                    MimePart::new("multipart/mixed", parts)
447                }
448                (Some(text), None, None) => text,
449                (None, Some(html), Some(attachments)) => {
450                    let mut parts = Vec::with_capacity(attachments.len() + 1);
451                    parts.push(html);
452                    parts.extend(attachments);
453                    MimePart::new("multipart/mixed", parts)
454                }
455                (None, Some(html), None) => html,
456                (None, None, Some(attachments)) => MimePart::new("multipart/mixed", attachments),
457                (None, None, None) => MimePart::new("text/plain", "\n"),
458            }
459        })
460        .write_part(output)?;
461
462        Ok(())
463    }
464
465    /// Build message to a Vec<u8>.
466    pub fn write_to_vec(self) -> io::Result<Vec<u8>> {
467        let mut output = Vec::new();
468        self.write_to(&mut output)?;
469        Ok(output)
470    }
471
472    /// Build message to a String.
473    pub fn write_to_string(self) -> io::Result<String> {
474        let mut output = Vec::new();
475        self.write_to(&mut output)?;
476        String::from_utf8(output).map_err(|err| io::Error::new(io::ErrorKind::Other, err))
477    }
478}
479
480// #[cfg(test)]
481// mod tests {
482
483//     use mail_parser::MessageParser;
484
485//     use crate::{
486//         headers::{address::Address, url::URL},
487//         mime::MimePart,
488//         MessageBuilder,
489//     };
490
491//     #[test]
492//     fn build_nested_message() {
493//         let output = MessageBuilder::new()
494//             .from(Address::new_address("John Doe".into(), "john@doe.com"))
495//             .to(Address::new_address("Jane Doe".into(), "jane@doe.com"))
496//             .subject("RFC 8621 Section 4.1.4 test")
497//             .body(MimePart::new(
498//                 "multipart/mixed",
499//                 vec![
500//                     MimePart::new("text/plain", "Part A contents go here...").inline(),
501//                     MimePart::new(
502//                         "multipart/mixed",
503//                         vec![
504//                             MimePart::new(
505//                                 "multipart/alternative",
506//                                 vec![
507//                                     MimePart::new(
508//                                         "multipart/mixed",
509//                                         vec![
510//                                             MimePart::new(
511//                                                 "text/plain",
512//                                                 "Part B contents go here...",
513//                                             )
514//                                             .inline(),
515//                                             MimePart::new(
516//                                                 "image/jpeg",
517//                                                 "Part C contents go here...".as_bytes(),
518//                                             )
519//                                             .inline(),
520//                                             MimePart::new(
521//                                                 "text/plain",
522//                                                 "Part D contents go here...",
523//                                             )
524//                                             .inline(),
525//                                         ],
526//                                     ),
527//                                     MimePart::new(
528//                                         "multipart/related",
529//                                         vec![
530//                                             MimePart::new(
531//                                                 "text/html",
532//                                                 "Part E contents go here...",
533//                                             )
534//                                             .inline(),
535//                                             MimePart::new(
536//                                                 "image/jpeg",
537//                                                 "Part F contents go here...".as_bytes(),
538//                                             ),
539//                                         ],
540//                                     ),
541//                                 ],
542//                             ),
543//                             MimePart::new("image/jpeg", "Part G contents go here...".as_bytes())
544//                                 .attachment("image_G.jpg"),
545//                             MimePart::new(
546//                                 "application/x-excel",
547//                                 "Part H contents go here...".as_bytes(),
548//                             ),
549//                             MimePart::new(
550//                                 "x-message/rfc822",
551//                                 "Part J contents go here...".as_bytes(),
552//                             ),
553//                         ],
554//                     ),
555//                     MimePart::new("text/plain", "Part K contents go here...").inline(),
556//                 ],
557//             ))
558//             .write_to_vec()
559//             .unwrap();
560//         MessageParser::new().parse(&output).unwrap();
561//         //fs::write("test.yaml", &serde_yaml::to_string(&message).unwrap()).unwrap();
562//     }
563
564//     #[test]
565//     fn build_message() {
566//         let output = MessageBuilder::new()
567//             .from(("John Doe", "john@doe.com"))
568//             .to(vec![
569//                 ("Antoine de Saint-Exupéry", "antoine@exupery.com"),
570//                 ("안녕하세요 세계", "test@test.com"),
571//                 ("Xin chào", "addr@addr.com"),
572//             ])
573//             .bcc(vec![
574//                 (
575//                     "Привет, мир",
576//                     vec![
577//                         ("ASCII recipient", "addr1@addr7.com"),
578//                         ("ハロー・ワールド", "addr2@addr6.com"),
579//                         ("áéíóú", "addr3@addr5.com"),
580//                         ("Γειά σου Κόσμε", "addr4@addr4.com"),
581//                     ],
582//                 ),
583//                 (
584//                     "Hello world",
585//                     vec![
586//                         ("שלום עולם", "addr5@addr3.com"),
587//                         ("¡El ñandú comió ñoquis!", "addr6@addr2.com"),
588//                         ("Recipient", "addr7@addr1.com"),
589//                     ],
590//                 ),
591//             ])
592//             .header("List-Archive", URL::new("http://example.com/archive"))
593//             .subject("Hello world!")
594//             .text_body("Hello, world!\n".repeat(20))
595//             .html_body("<p>¡Hola Mundo!</p>".repeat(20))
596//             .inline("image/png", "cid:image", [0, 1, 2, 3, 4, 5].as_ref())
597//             .attachment("text/plain", "my fíle.txt", "안녕하세요 세계".repeat(20))
598//             .attachment(
599//                 "text/plain",
600//                 "ハロー・ワールド",
601//                 "ハロー・ワールド".repeat(20).into_bytes(),
602//             )
603//             .write_to_vec()
604//             .unwrap();
605//         MessageParser::new().parse(&output).unwrap();
606//     }
607// }