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}