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//! [](https://crates.io/crates/mail-builder)
15//! [](https://github.com/stalwartlabs/mail-builder/actions/workflows/rust.yml)
16//! [](https://docs.rs/mail-builder)
17//! [](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// }