lettre/message/
mod.rs

1//! Provides a strongly typed way to build emails
2//!
3//! ## Usage
4//!
5//! This section demonstrates how to build messages.
6//!
7//! <style>
8//! summary, details:not([open]) { cursor: pointer; }
9//! </style>
10//!
11//!
12//! ### Plain body
13//!
14//! The easiest way of creating a message, which uses a plain text body.
15//!
16//! ```rust
17//! use lettre::message::{header::ContentType, Message};
18//!
19//! # use std::error::Error;
20//! # fn main() -> Result<(), Box<dyn Error>> {
21//! let m = Message::builder()
22//!     .from("NoBody <nobody@domain.tld>".parse()?)
23//!     .reply_to("Yuin <yuin@domain.tld>".parse()?)
24//!     .to("Hei <hei@domain.tld>".parse()?)
25//!     .subject("Happy new year")
26//!     .header(ContentType::TEXT_PLAIN)
27//!     .body(String::from("Be happy!"))?;
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! Which produces:
33//! <details>
34//! <summary>Click to expand</summary>
35//!
36//! ```sh
37//! From: NoBody <nobody@domain.tld>
38//! Reply-To: Yuin <yuin@domain.tld>
39//! To: Hei <hei@domain.tld>
40//! Subject: Happy new year
41//! Date: Sat, 12 Dec 2020 16:33:19 GMT
42//! Content-Type: text/plain; charset=utf-8
43//! Content-Transfer-Encoding: 7bit
44//!
45//! Be happy!
46//! ```
47//! </details>
48//! <br />
49//!
50//! The unicode header data is encoded using _UTF8-Base64_ encoding, when necessary.
51//!
52//! The `Content-Transfer-Encoding` is chosen based on the best encoding
53//! available for the given body, between `7bit`, `quoted-printable` and `base64`.
54//!
55//! ### Plain and HTML body
56//!
57//! Uses a MIME body to include both plain text and HTML versions of the body.
58//!
59//! ```rust
60//! # use std::error::Error;
61//! use lettre::message::{header, Message, MultiPart, SinglePart};
62//!
63//! # fn main() -> Result<(), Box<dyn Error>> {
64//! let m = Message::builder()
65//!     .from("NoBody <nobody@domain.tld>".parse()?)
66//!     .reply_to("Yuin <yuin@domain.tld>".parse()?)
67//!     .to("Hei <hei@domain.tld>".parse()?)
68//!     .subject("Happy new year")
69//!     .multipart(MultiPart::alternative_plain_html(
70//!         String::from("Hello, world! :)"),
71//!         String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
72//!     ))?;
73//! # Ok(())
74//! # }
75//! ```
76//!
77//! Which produces:
78//! <details>
79//! <summary>Click to expand</summary>
80//!
81//! ```sh
82//! From: NoBody <nobody@domain.tld>
83//! Reply-To: Yuin <yuin@domain.tld>
84//! To: Hei <hei@domain.tld>
85//! Subject: Happy new year
86//! MIME-Version: 1.0
87//! Date: Sat, 12 Dec 2020 16:33:19 GMT
88//! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
89//!
90//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
91//! Content-Type: text/plain; charset=utf8
92//! Content-Transfer-Encoding: 7bit
93//!
94//! Hello, world! :)
95//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
96//! Content-Type: text/html; charset=utf8
97//! Content-Transfer-Encoding: 7bit
98//!
99//! <p><b>Hello</b>, <i>world</i>! <img src="cid:123"></p>
100//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
101//! ```
102//! </details>
103//!
104//! ### Complex MIME body
105//!
106//! This example shows how to include both plain and HTML versions of the body,
107//! attachments and inlined images.
108//!
109//! ```rust
110//! # use std::error::Error;
111//! use std::fs;
112//!
113//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
114//!
115//! # fn main() -> Result<(), Box<dyn Error>> {
116//! let image = fs::read("docs/lettre.png")?;
117//! // this image_body can be cloned and reused between emails.
118//! // since `Body` holds a pre-encoded body, reusing it means avoiding having
119//! // to re-encode the same body for every email (this clearly only applies
120//! // when sending multiple emails with the same attachment).
121//! let image_body = Body::new(image);
122//!
123//! let m = Message::builder()
124//!     .from("NoBody <nobody@domain.tld>".parse()?)
125//!     .reply_to("Yuin <yuin@domain.tld>".parse()?)
126//!     .to("Hei <hei@domain.tld>".parse()?)
127//!     .subject("Happy new year")
128//!     .multipart(
129//!         MultiPart::mixed()
130//!             .multipart(
131//!                 MultiPart::alternative()
132//!                     .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
133//!                     .multipart(
134//!                         MultiPart::related()
135//!                             .singlepart(SinglePart::html(String::from(
136//!                                 "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
137//!                             )))
138//!                             .singlepart(
139//!                                 Attachment::new_inline(String::from("123"))
140//!                                     .body(image_body, "image/png".parse().unwrap()),
141//!                             ),
142//!                     ),
143//!             )
144//!             .singlepart(Attachment::new(String::from("example.rs")).body(
145//!                 String::from("fn main() { println!(\"Hello, World!\") }"),
146//!                 "text/plain".parse().unwrap(),
147//!             )),
148//!     )?;
149//! # Ok(())
150//! # }
151//! ```
152//!
153//! Which produces:
154//! <details>
155//! <summary>Click to expand</summary>
156//!
157//! ```sh
158//! From: NoBody <nobody@domain.tld>
159//! Reply-To: Yuin <yuin@domain.tld>
160//! To: Hei <hei@domain.tld>
161//! Subject: Happy new year
162//! MIME-Version: 1.0
163//! Date: Sat, 12 Dec 2020 16:30:45 GMT
164//! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
165//!
166//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
167//! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk"
168//!
169//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
170//! Content-Type: text/plain; charset=utf8
171//! Content-Transfer-Encoding: 7bit
172//!
173//! Hello, world! :)
174//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
175//! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr"
176//!
177//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
178//! Content-Type: text/html; charset=utf8
179//! Content-Transfer-Encoding: 7bit
180//!
181//! <p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
182//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
183//! Content-Type: image/png
184//! Content-Disposition: inline
185//! Content-ID: <123>
186//! Content-Transfer-Encoding: base64
187//!
188//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
189//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr--
190//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk--
191//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
192//! Content-Type: text/plain; charset=utf8
193//! Content-Disposition: attachment; filename="example.rs"
194//! Content-Transfer-Encoding: 7bit
195//!
196//! fn main() { println!("Hello, World!") }
197//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
198//! ```
199//! </details>
200
201use std::{io::Write, iter, time::SystemTime};
202
203pub use attachment::Attachment;
204pub use body::{Body, IntoBody, MaybeString};
205#[cfg(feature = "dkim")]
206pub use dkim::*;
207pub use mailbox::*;
208pub use mimebody::*;
209
210mod attachment;
211mod body;
212#[cfg(feature = "dkim")]
213pub mod dkim;
214pub mod header;
215mod mailbox;
216mod mimebody;
217
218use crate::{
219    address::Envelope,
220    message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
221    Error as EmailError,
222};
223
224const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
225
226/// Something that can be formatted as an email message
227trait EmailFormat {
228    // Use a writer?
229    fn format(&self, out: &mut Vec<u8>);
230}
231
232/// A builder for messages
233#[derive(Debug, Clone)]
234pub struct MessageBuilder {
235    headers: Headers,
236    envelope: Option<Envelope>,
237    drop_bcc: bool,
238}
239
240impl MessageBuilder {
241    /// Creates a new default message builder
242    pub fn new() -> Self {
243        Self {
244            headers: Headers::new(),
245            envelope: None,
246            drop_bcc: true,
247        }
248    }
249
250    /// Set or add mailbox to `From` header
251    ///
252    /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
253    ///
254    /// Shortcut for `self.mailbox(header::From(mbox))`.
255    pub fn from(self, mbox: Mailbox) -> Self {
256        self.mailbox(header::From::from(Mailboxes::from(mbox)))
257    }
258
259    /// Set `Sender` header. Should be used when providing several `From` mailboxes.
260    ///
261    /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
262    ///
263    /// Shortcut for `self.header(header::Sender(mbox))`.
264    pub fn sender(self, mbox: Mailbox) -> Self {
265        self.header(header::Sender::from(mbox))
266    }
267
268    /// Add `Date` header to message
269    ///
270    /// Shortcut for `self.header(header::Date::new(st))`.
271    pub fn date(self, st: SystemTime) -> Self {
272        self.header(header::Date::new(st))
273    }
274
275    /// Set `Date` header using current date/time
276    ///
277    /// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
278    /// if no date has been provided.
279    pub fn date_now(self) -> Self {
280        self.date(crate::time::now())
281    }
282
283    /// Set or add mailbox to `ReplyTo` header
284    ///
285    /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
286    ///
287    /// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
288    pub fn reply_to(self, mbox: Mailbox) -> Self {
289        self.mailbox(header::ReplyTo(mbox.into()))
290    }
291
292    /// Set or add mailbox to `To` header
293    ///
294    /// Shortcut for `self.mailbox(header::To(mbox))`.
295    pub fn to(self, mbox: Mailbox) -> Self {
296        self.mailbox(header::To(mbox.into()))
297    }
298
299    /// Set or add mailbox to `Cc` header
300    ///
301    /// Shortcut for `self.mailbox(header::Cc(mbox))`.
302    pub fn cc(self, mbox: Mailbox) -> Self {
303        self.mailbox(header::Cc(mbox.into()))
304    }
305
306    /// Set or add mailbox to `Bcc` header
307    ///
308    /// Shortcut for `self.mailbox(header::Bcc(mbox))`.
309    pub fn bcc(self, mbox: Mailbox) -> Self {
310        self.mailbox(header::Bcc(mbox.into()))
311    }
312
313    /// Set or add message id to [`In-Reply-To`
314    /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
315    pub fn in_reply_to(self, id: String) -> Self {
316        self.header(header::InReplyTo::from(id))
317    }
318
319    /// Set or add message id to [`References`
320    /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
321    pub fn references(self, id: String) -> Self {
322        self.header(header::References::from(id))
323    }
324
325    /// Set `Subject` header to message
326    ///
327    /// Shortcut for `self.header(header::Subject(subject.into()))`.
328    pub fn subject<S: Into<String>>(self, subject: S) -> Self {
329        let s: String = subject.into();
330        self.header(header::Subject::from(s))
331    }
332
333    /// Set [Message-ID
334    /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
335    ///
336    /// Should generally be inserted by the mail relay.
337    ///
338    /// If `None` is provided, an id will be generated in the
339    /// `<UUID@HOSTNAME>`.
340    pub fn message_id(self, id: Option<String>) -> Self {
341        match id {
342            Some(i) => self.header(header::MessageId::from(i)),
343            None => {
344                #[cfg(feature = "hostname")]
345                let hostname = hostname::get()
346                    .map_err(|_| ())
347                    .and_then(|s| s.into_string().map_err(|_| ()))
348                    .unwrap_or_else(|()| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
349                #[cfg(not(feature = "hostname"))]
350                let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
351
352                self.header(header::MessageId::from(
353                    // https://tools.ietf.org/html/rfc5322#section-3.6.4
354                    format!("<{}@{}>", make_message_id(), hostname),
355                ))
356            }
357        }
358    }
359
360    /// Set [User-Agent
361    /// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00)
362    pub fn user_agent(self, id: String) -> Self {
363        self.header(header::UserAgent::from(id))
364    }
365
366    /// Set custom header to message
367    pub fn header<H: Header>(mut self, header: H) -> Self {
368        self.headers.set(header);
369        self
370    }
371
372    /// Add mailbox to header
373    pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
374        match self.headers.get::<H>() {
375            Some(mut header_) => {
376                header_.join_mailboxes(header);
377                self.header(header_)
378            }
379            None => self.header(header),
380        }
381    }
382
383    /// Force specific envelope (by default it is derived from headers)
384    pub fn envelope(mut self, envelope: Envelope) -> Self {
385        self.envelope = Some(envelope);
386        self
387    }
388
389    /// Keep the `Bcc` header
390    ///
391    /// By default, the `Bcc` header is removed from the email after
392    /// using it to generate the message envelope. In some cases though,
393    /// like when saving the email as an `.eml`, or sending through
394    /// some transports (like the Gmail API) that don't take a separate
395    /// envelope value, it becomes necessary to keep the `Bcc` header.
396    ///
397    /// Calling this method overrides the default behavior.
398    pub fn keep_bcc(mut self) -> Self {
399        self.drop_bcc = false;
400        self
401    }
402
403    // TODO: High-level methods for attachments and embedded files
404
405    /// Create message from body
406    fn build(self, body: MessageBody) -> Result<Message, EmailError> {
407        // Check for missing required headers
408        // https://tools.ietf.org/html/rfc5322#section-3.6
409
410        // Insert Date if missing
411        let mut res = if self.headers.get::<header::Date>().is_none() {
412            self.date_now()
413        } else {
414            self
415        };
416
417        // Fail is missing correct originator (Sender or From)
418        match res.headers.get::<header::From>() {
419            Some(header::From(f)) => {
420                let from: Vec<Mailbox> = f.into();
421                if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
422                    return Err(EmailError::TooManyFrom);
423                }
424            }
425            None => {
426                return Err(EmailError::MissingFrom);
427            }
428        }
429
430        let envelope = match res.envelope {
431            Some(e) => e,
432            None => Envelope::try_from(&res.headers)?,
433        };
434
435        if res.drop_bcc {
436            // Remove `Bcc` headers now the envelope is set
437            res.headers.remove::<header::Bcc>();
438        }
439
440        Ok(Message {
441            headers: res.headers,
442            body,
443            envelope,
444        })
445    }
446
447    /// Create [`Message`] using a [`Vec<u8>`], [`String`], or [`Body`] body
448    ///
449    /// Automatically gets encoded with `7bit`, `quoted-printable` or `base64`
450    /// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
451    /// for `body`.
452    pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
453        let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
454        let body = body.into_body(maybe_encoding);
455
456        self.headers.set(body.encoding());
457        self.build(MessageBody::Raw(body.into_vec()))
458    }
459
460    /// Create message using mime body ([`MultiPart`])
461    pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
462        self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
463    }
464
465    /// Create message using mime body ([`SinglePart`])
466    pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
467        self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
468    }
469
470    /// Set `MIME-Version` header to 1.0
471    ///
472    /// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
473    ///
474    /// Not exposed as it is set by body methods
475    fn mime_1_0(self) -> Self {
476        self.header(header::MIME_VERSION_1_0)
477    }
478}
479
480/// Email message which can be formatted
481#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
482#[derive(Clone, Debug)]
483pub struct Message {
484    headers: Headers,
485    body: MessageBody,
486    envelope: Envelope,
487}
488
489#[derive(Clone, Debug)]
490enum MessageBody {
491    Mime(Part),
492    Raw(Vec<u8>),
493}
494
495impl Message {
496    /// Create a new message builder without headers
497    pub fn builder() -> MessageBuilder {
498        MessageBuilder::new()
499    }
500
501    /// Get the headers from the Message
502    pub fn headers(&self) -> &Headers {
503        &self.headers
504    }
505
506    /// Get a mutable reference to the headers
507    pub fn headers_mut(&mut self) -> &mut Headers {
508        &mut self.headers
509    }
510
511    /// Get `Message` envelope
512    pub fn envelope(&self) -> &Envelope {
513        &self.envelope
514    }
515
516    /// Get message content formatted for SMTP
517    pub fn formatted(&self) -> Vec<u8> {
518        let mut out = Vec::new();
519        self.format(&mut out);
520        out
521    }
522
523    #[cfg(feature = "dkim")]
524    /// Format body for signing
525    pub(crate) fn body_raw(&self) -> Vec<u8> {
526        let mut out = Vec::new();
527        match &self.body {
528            MessageBody::Mime(p) => p.format_body(&mut out),
529            MessageBody::Raw(r) => out.extend_from_slice(r),
530        }
531        out.extend_from_slice(b"\r\n");
532        out
533    }
534
535    /// Sign the message using Dkim
536    ///
537    /// Example:
538    /// ```rust
539    /// use lettre::{
540    ///     message::{
541    ///         dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
542    ///         header::ContentType,
543    ///     },
544    ///     Message,
545    /// };
546    ///
547    /// let mut message = Message::builder()
548    ///     .from("Alice <alice@example.org>".parse().unwrap())
549    ///     .reply_to("Bob <bob@example.org>".parse().unwrap())
550    ///     .to("Carla <carla@example.net>".parse().unwrap())
551    ///     .subject("Hello")
552    ///     .header(ContentType::TEXT_PLAIN)
553    ///     .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
554    ///     .unwrap();
555    /// let key = "-----BEGIN RSA PRIVATE KEY-----
556    /// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
557    /// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL
558    /// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4
559    /// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA
560    /// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU
561    /// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo
562    /// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc
563    /// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM
564    /// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX
565    /// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR
566    /// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L
567    /// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg
568    /// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK
569    /// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F
570    /// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN
571    /// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q
572    /// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x
573    /// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW
574    /// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu
575    /// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/
576    /// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo
577    /// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI
578    /// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX
579    /// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
580    /// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
581    /// -----END RSA PRIVATE KEY-----";
582    /// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
583    /// message.sign(&DkimConfig::default_config(
584    ///     "dkimtest".to_owned(),
585    ///     "example.org".to_owned(),
586    ///     signing_key,
587    /// ));
588    /// println!(
589    ///     "message: {}",
590    ///     std::str::from_utf8(&message.formatted()).unwrap()
591    /// );
592    /// ```
593    #[cfg(feature = "dkim")]
594    pub fn sign(&mut self, dkim_config: &DkimConfig) {
595        dkim_sign(self, dkim_config);
596    }
597}
598
599impl EmailFormat for Message {
600    fn format(&self, out: &mut Vec<u8>) {
601        write!(out, "{}", self.headers)
602            .expect("A Write implementation panicked while formatting headers");
603
604        match &self.body {
605            MessageBody::Mime(p) => p.format(out),
606            MessageBody::Raw(r) => {
607                out.extend_from_slice(b"\r\n");
608                out.extend_from_slice(r);
609            }
610        }
611    }
612}
613
614impl Default for MessageBuilder {
615    fn default() -> Self {
616        MessageBuilder::new()
617    }
618}
619
620/// Create a random message id.
621/// (Not cryptographically random)
622fn make_message_id() -> String {
623    iter::repeat_with(fastrand::alphanumeric).take(36).collect()
624}
625
626#[cfg(test)]
627mod test {
628    use std::time::{Duration, SystemTime};
629
630    use pretty_assertions::assert_eq;
631
632    use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
633
634    #[test]
635    fn email_missing_originator() {
636        assert!(Message::builder()
637            .body(String::from("Happy new year!"))
638            .is_err());
639    }
640
641    #[test]
642    fn email_minimal_message() {
643        assert!(Message::builder()
644            .from("NoBody <nobody@domain.tld>".parse().unwrap())
645            .to("NoBody <nobody@domain.tld>".parse().unwrap())
646            .body(String::from("Happy new year!"))
647            .is_ok());
648    }
649
650    #[test]
651    fn email_missing_sender() {
652        assert!(Message::builder()
653            .from("NoBody <nobody@domain.tld>".parse().unwrap())
654            .from("AnyBody <anybody@domain.tld>".parse().unwrap())
655            .body(String::from("Happy new year!"))
656            .is_err());
657    }
658
659    #[test]
660    fn email_message_no_bcc() {
661        // Tue, 15 Nov 1994 08:12:31 GMT
662        let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
663
664        let email = Message::builder()
665            .date(date)
666            .bcc("hidden@example.com".parse().unwrap())
667            .header(header::From(
668                vec![Mailbox::new(
669                    Some("Каи".into()),
670                    "kayo@example.com".parse().unwrap(),
671                )]
672                .into(),
673            ))
674            .header(header::To(
675                vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
676            ))
677            .header(header::Subject::from(String::from("яңа ел белән!")))
678            .body(String::from("Happy new year!"))
679            .unwrap();
680
681        assert_eq!(
682            String::from_utf8(email.formatted()).unwrap(),
683            concat!(
684                "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
685                "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
686                "To: \"Pony O.P.\" <pony@domain.tld>\r\n",
687                "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
688                "Content-Transfer-Encoding: 7bit\r\n",
689                "\r\n",
690                "Happy new year!"
691            )
692        );
693    }
694
695    #[test]
696    fn email_message_keep_bcc() {
697        // Tue, 15 Nov 1994 08:12:31 GMT
698        let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
699
700        let email = Message::builder()
701            .date(date)
702            .bcc("hidden@example.com".parse().unwrap())
703            .keep_bcc()
704            .header(header::From(
705                vec![Mailbox::new(
706                    Some("Каи".into()),
707                    "kayo@example.com".parse().unwrap(),
708                )]
709                .into(),
710            ))
711            .header(header::To(
712                vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
713            ))
714            .header(header::Subject::from(String::from("яңа ел белән!")))
715            .body(String::from("Happy new year!"))
716            .unwrap();
717
718        assert_eq!(
719            String::from_utf8(email.formatted()).unwrap(),
720            concat!(
721                "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
722                "Bcc: hidden@example.com\r\n",
723                "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
724                "To: \"Pony O.P.\" <pony@domain.tld>\r\n",
725                "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
726                "Content-Transfer-Encoding: 7bit\r\n",
727                "\r\n",
728                "Happy new year!"
729            )
730        );
731    }
732
733    #[test]
734    fn email_with_png() {
735        // Tue, 15 Nov 1994 08:12:31 GMT
736        let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
737        let img = std::fs::read("./docs/lettre.png").unwrap();
738        let m = Message::builder()
739            .date(date)
740            .from("NoBody <nobody@domain.tld>".parse().unwrap())
741            .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
742            .to("Hei <hei@domain.tld>".parse().unwrap())
743            .subject("Happy new year")
744            .multipart(
745                MultiPart::related()
746                    .singlepart(
747                        SinglePart::builder()
748                            .header(header::ContentType::TEXT_HTML)
749                            .body(String::from(
750                                "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
751                            )),
752                    )
753                    .singlepart(
754                        SinglePart::builder()
755                            .header(header::ContentType::parse("image/png").unwrap())
756                            .header(header::ContentDisposition::inline())
757                            .header(header::ContentId::from(String::from("<123>")))
758                            .body(img),
759                    ),
760            )
761            .unwrap();
762
763        let output = String::from_utf8(m.formatted()).unwrap();
764        let file_expected = std::fs::read("./testdata/email_with_png.eml").unwrap();
765        let expected = String::from_utf8(file_expected).unwrap();
766
767        for (i, line) in output.lines().zip(expected.lines()).enumerate() {
768            if i == 7 || i == 9 || i == 14 || i == 233 {
769                continue;
770            }
771
772            assert_eq!(line.0, line.1);
773        }
774    }
775
776    #[test]
777    fn test_make_message_id() {
778        let mut ids = std::collections::HashSet::with_capacity(10);
779        for _ in 0..1000 {
780            ids.insert(make_message_id());
781        }
782
783        // Ensure there are no duplicates
784        assert_eq!(1000, ids.len());
785
786        // Ensure correct length
787        for id in ids {
788            assert_eq!(36, id.len());
789        }
790    }
791}