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, HeaderValue, 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    /// Set raw custom header to message
373    pub fn raw_header(mut self, raw_header: HeaderValue) -> Self {
374        self.headers.insert_raw(raw_header);
375        self
376    }
377
378    /// Add mailbox to header
379    pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
380        match self.headers.get::<H>() {
381            Some(mut header_) => {
382                header_.join_mailboxes(header);
383                self.header(header_)
384            }
385            None => self.header(header),
386        }
387    }
388
389    /// Force specific envelope (by default it is derived from headers)
390    pub fn envelope(mut self, envelope: Envelope) -> Self {
391        self.envelope = Some(envelope);
392        self
393    }
394
395    /// Keep the `Bcc` header
396    ///
397    /// By default, the `Bcc` header is removed from the email after
398    /// using it to generate the message envelope. In some cases though,
399    /// like when saving the email as an `.eml`, or sending through
400    /// some transports (like the Gmail API) that don't take a separate
401    /// envelope value, it becomes necessary to keep the `Bcc` header.
402    ///
403    /// Calling this method overrides the default behavior.
404    pub fn keep_bcc(mut self) -> Self {
405        self.drop_bcc = false;
406        self
407    }
408
409    // TODO: High-level methods for attachments and embedded files
410
411    /// Create message from body
412    fn build(self, body: MessageBody) -> Result<Message, EmailError> {
413        // Check for missing required headers
414        // https://tools.ietf.org/html/rfc5322#section-3.6
415
416        // Insert Date if missing
417        let mut res = if self.headers.get::<header::Date>().is_none() {
418            self.date_now()
419        } else {
420            self
421        };
422
423        // Fail is missing correct originator (Sender or From)
424        match res.headers.get::<header::From>() {
425            Some(header::From(f)) => {
426                let from: Vec<Mailbox> = f.into();
427                if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
428                    return Err(EmailError::TooManyFrom);
429                }
430            }
431            None => {
432                return Err(EmailError::MissingFrom);
433            }
434        }
435
436        let envelope = match res.envelope {
437            Some(e) => e,
438            None => Envelope::try_from(&res.headers)?,
439        };
440
441        if res.drop_bcc {
442            // Remove `Bcc` headers now the envelope is set
443            res.headers.remove::<header::Bcc>();
444        }
445
446        Ok(Message {
447            headers: res.headers,
448            body,
449            envelope,
450        })
451    }
452
453    /// Create [`Message`] using a [`Vec<u8>`], [`String`], or [`Body`] body
454    ///
455    /// Automatically gets encoded with `7bit`, `quoted-printable` or `base64`
456    /// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
457    /// for `body`.
458    pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
459        let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
460        let body = body.into_body(maybe_encoding);
461
462        self.headers.set(body.encoding());
463        self.build(MessageBody::Raw(body.into_vec()))
464    }
465
466    /// Create message using mime body ([`MultiPart`])
467    pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
468        self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
469    }
470
471    /// Create message using mime body ([`SinglePart`])
472    pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
473        self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
474    }
475
476    /// Set `MIME-Version` header to 1.0
477    ///
478    /// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
479    ///
480    /// Not exposed as it is set by body methods
481    fn mime_1_0(self) -> Self {
482        self.header(header::MIME_VERSION_1_0)
483    }
484}
485
486/// Email message which can be formatted
487#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
488#[derive(Clone, Debug)]
489pub struct Message {
490    headers: Headers,
491    body: MessageBody,
492    envelope: Envelope,
493}
494
495#[derive(Clone, Debug)]
496enum MessageBody {
497    Mime(Part),
498    Raw(Vec<u8>),
499}
500
501impl Message {
502    /// Create a new message builder without headers
503    pub fn builder() -> MessageBuilder {
504        MessageBuilder::new()
505    }
506
507    /// Get the headers from the Message
508    pub fn headers(&self) -> &Headers {
509        &self.headers
510    }
511
512    /// Get a mutable reference to the headers
513    pub fn headers_mut(&mut self) -> &mut Headers {
514        &mut self.headers
515    }
516
517    /// Get `Message` envelope
518    pub fn envelope(&self) -> &Envelope {
519        &self.envelope
520    }
521
522    /// Get message content formatted for SMTP
523    pub fn formatted(&self) -> Vec<u8> {
524        let mut out = Vec::new();
525        self.format(&mut out);
526        out
527    }
528
529    #[cfg(feature = "dkim")]
530    /// Format body for signing
531    pub(crate) fn body_raw(&self) -> Vec<u8> {
532        let mut out = Vec::new();
533        match &self.body {
534            MessageBody::Mime(p) => p.format_body(&mut out),
535            MessageBody::Raw(r) => out.extend_from_slice(r),
536        }
537        out.extend_from_slice(b"\r\n");
538        out
539    }
540
541    /// Sign the message using Dkim
542    ///
543    /// Example:
544    /// ```rust
545    /// use lettre::{
546    ///     message::{
547    ///         dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
548    ///         header::ContentType,
549    ///     },
550    ///     Message,
551    /// };
552    ///
553    /// let mut message = Message::builder()
554    ///     .from("Alice <alice@example.org>".parse().unwrap())
555    ///     .reply_to("Bob <bob@example.org>".parse().unwrap())
556    ///     .to("Carla <carla@example.net>".parse().unwrap())
557    ///     .subject("Hello")
558    ///     .header(ContentType::TEXT_PLAIN)
559    ///     .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
560    ///     .unwrap();
561    /// let key = "-----BEGIN RSA PRIVATE KEY-----
562    /// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
563    /// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL
564    /// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4
565    /// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA
566    /// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU
567    /// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo
568    /// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc
569    /// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM
570    /// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX
571    /// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR
572    /// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L
573    /// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg
574    /// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK
575    /// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F
576    /// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN
577    /// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q
578    /// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x
579    /// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW
580    /// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu
581    /// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/
582    /// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo
583    /// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI
584    /// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX
585    /// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
586    /// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
587    /// -----END RSA PRIVATE KEY-----";
588    /// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
589    /// message.sign(&DkimConfig::default_config(
590    ///     "dkimtest".to_owned(),
591    ///     "example.org".to_owned(),
592    ///     signing_key,
593    /// ));
594    /// println!(
595    ///     "message: {}",
596    ///     std::str::from_utf8(&message.formatted()).unwrap()
597    /// );
598    /// ```
599    #[cfg(feature = "dkim")]
600    pub fn sign(&mut self, dkim_config: &DkimConfig) {
601        dkim_sign(self, dkim_config);
602    }
603}
604
605impl EmailFormat for Message {
606    fn format(&self, out: &mut Vec<u8>) {
607        write!(out, "{}", self.headers)
608            .expect("A Write implementation panicked while formatting headers");
609
610        match &self.body {
611            MessageBody::Mime(p) => p.format(out),
612            MessageBody::Raw(r) => {
613                out.extend_from_slice(b"\r\n");
614                out.extend_from_slice(r);
615            }
616        }
617    }
618}
619
620impl Default for MessageBuilder {
621    fn default() -> Self {
622        MessageBuilder::new()
623    }
624}
625
626/// Create a random message id.
627/// (Not cryptographically random)
628fn make_message_id() -> String {
629    iter::repeat_with(fastrand::alphanumeric).take(36).collect()
630}
631
632#[cfg(test)]
633mod test {
634    use std::time::{Duration, SystemTime};
635
636    use pretty_assertions::assert_eq;
637
638    use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
639
640    #[test]
641    fn email_missing_originator() {
642        assert!(Message::builder()
643            .body(String::from("Happy new year!"))
644            .is_err());
645    }
646
647    #[test]
648    fn email_minimal_message() {
649        assert!(Message::builder()
650            .from("NoBody <nobody@domain.tld>".parse().unwrap())
651            .to("NoBody <nobody@domain.tld>".parse().unwrap())
652            .body(String::from("Happy new year!"))
653            .is_ok());
654    }
655
656    #[test]
657    fn email_missing_sender() {
658        assert!(Message::builder()
659            .from("NoBody <nobody@domain.tld>".parse().unwrap())
660            .from("AnyBody <anybody@domain.tld>".parse().unwrap())
661            .body(String::from("Happy new year!"))
662            .is_err());
663    }
664
665    #[test]
666    fn email_message_no_bcc() {
667        // Tue, 15 Nov 1994 08:12:31 GMT
668        let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
669
670        let email = Message::builder()
671            .date(date)
672            .bcc("hidden@example.com".parse().unwrap())
673            .header(header::From(
674                vec![Mailbox::new(
675                    Some("Каи".into()),
676                    "kayo@example.com".parse().unwrap(),
677                )]
678                .into(),
679            ))
680            .header(header::To(
681                vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
682            ))
683            .header(header::Subject::from(String::from("яңа ел белән!")))
684            .body(String::from("Happy new year!"))
685            .unwrap();
686
687        assert_eq!(
688            String::from_utf8(email.formatted()).unwrap(),
689            concat!(
690                "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
691                "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
692                "To: \"Pony O.P.\" <pony@domain.tld>\r\n",
693                "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
694                "Content-Transfer-Encoding: 7bit\r\n",
695                "\r\n",
696                "Happy new year!"
697            )
698        );
699    }
700
701    #[test]
702    fn email_message_keep_bcc() {
703        // Tue, 15 Nov 1994 08:12:31 GMT
704        let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
705
706        let email = Message::builder()
707            .date(date)
708            .bcc("hidden@example.com".parse().unwrap())
709            .keep_bcc()
710            .header(header::From(
711                vec![Mailbox::new(
712                    Some("Каи".into()),
713                    "kayo@example.com".parse().unwrap(),
714                )]
715                .into(),
716            ))
717            .header(header::To(
718                vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
719            ))
720            .raw_header(header::HeaderValue::new(
721                header::HeaderName::new_from_ascii_str("Subject"),
722                "яңа ел белән!".to_owned(),
723            ))
724            .body(String::from("Happy new year!"))
725            .unwrap();
726
727        assert_eq!(
728            String::from_utf8(email.formatted()).unwrap(),
729            concat!(
730                "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
731                "Bcc: hidden@example.com\r\n",
732                "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
733                "To: \"Pony O.P.\" <pony@domain.tld>\r\n",
734                "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
735                "Content-Transfer-Encoding: 7bit\r\n",
736                "\r\n",
737                "Happy new year!"
738            )
739        );
740    }
741
742    #[test]
743    fn email_with_png() {
744        // Tue, 15 Nov 1994 08:12:31 GMT
745        let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
746        let img = std::fs::read("./docs/lettre.png").unwrap();
747        let m = Message::builder()
748            .date(date)
749            .from("NoBody <nobody@domain.tld>".parse().unwrap())
750            .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
751            .to("Hei <hei@domain.tld>".parse().unwrap())
752            .subject("Happy new year")
753            .multipart(
754                MultiPart::related()
755                    .singlepart(
756                        SinglePart::builder()
757                            .header(header::ContentType::TEXT_HTML)
758                            .body(String::from(
759                                "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
760                            )),
761                    )
762                    .singlepart(
763                        SinglePart::builder()
764                            .header(header::ContentType::parse("image/png").unwrap())
765                            .header(header::ContentDisposition::inline())
766                            .header(header::ContentId::from(String::from("<123>")))
767                            .body(img),
768                    ),
769            )
770            .unwrap();
771
772        let output = String::from_utf8(m.formatted()).unwrap();
773        let file_expected = std::fs::read("./testdata/email_with_png.eml").unwrap();
774        let expected = String::from_utf8(file_expected).unwrap();
775
776        for (i, line) in output.lines().zip(expected.lines()).enumerate() {
777            if i == 7 || i == 9 || i == 14 || i == 233 {
778                continue;
779            }
780
781            assert_eq!(line.0, line.1);
782        }
783    }
784
785    #[test]
786    fn test_make_message_id() {
787        let mut ids = std::collections::HashSet::with_capacity(10);
788        for _ in 0..1000 {
789            ids.insert(make_message_id());
790        }
791
792        // Ensure there are no duplicates
793        assert_eq!(1000, ids.len());
794
795        // Ensure correct length
796        for id in ids {
797            assert_eq!(36, id.len());
798        }
799    }
800}