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}