//! Provides a strongly typed way to build emails //! //! ## Usage //! //! This section demonstrates how to build messages. //! //! //! //! //! ### Plain body //! //! The easiest way of creating a message, which uses a plain text body. //! //! ```rust //! use lettre::message::{header::ContentType, Message}; //! //! # use std::error::Error; //! # fn main() -> Result<(), Box> { //! let m = Message::builder() //! .from("NoBody ".parse()?) //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") //! .header(ContentType::TEXT_PLAIN) //! .body(String::from("Be happy!"))?; //! # Ok(()) //! # } //! ``` //! //! Which produces: //!
//! Click to expand //! //! ```sh //! From: NoBody //! Reply-To: Yuin //! To: Hei //! Subject: Happy new year //! Date: Sat, 12 Dec 2020 16:33:19 GMT //! Content-Type: text/plain; charset=utf-8 //! Content-Transfer-Encoding: 7bit //! //! Be happy! //! ``` //!
//!
//! //! The unicode header data is encoded using _UTF8-Base64_ encoding, when necessary. //! //! The `Content-Transfer-Encoding` is chosen based on the best encoding //! available for the given body, between `7bit`, `quoted-printable` and `base64`. //! //! ### Plain and HTML body //! //! Uses a MIME body to include both plain text and HTML versions of the body. //! //! ```rust //! # use std::error::Error; //! use lettre::message::{header, Message, MultiPart, SinglePart}; //! //! # fn main() -> Result<(), Box> { //! let m = Message::builder() //! .from("NoBody ".parse()?) //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") //! .multipart(MultiPart::alternative_plain_html( //! String::from("Hello, world! :)"), //! String::from("

Hello, world!

"), //! ))?; //! # Ok(()) //! # } //! ``` //! //! Which produces: //!
//! Click to expand //! //! ```sh //! From: NoBody //! Reply-To: Yuin //! To: Hei //! Subject: Happy new year //! MIME-Version: 1.0 //! Date: Sat, 12 Dec 2020 16:33:19 GMT //! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1" //! //! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 //! Content-Type: text/plain; charset=utf8 //! Content-Transfer-Encoding: 7bit //! //! Hello, world! :) //! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 //! Content-Type: text/html; charset=utf8 //! Content-Transfer-Encoding: 7bit //! //!

Hello, world!

//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1-- //! ``` //!
//! //! ### Complex MIME body //! //! This example shows how to include both plain and HTML versions of the body, //! attachments and inlined images. //! //! ```rust //! # use std::error::Error; //! use std::fs; //! //! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart}; //! //! # fn main() -> Result<(), Box> { //! let image = fs::read("docs/lettre.png")?; //! // this image_body can be cloned and reused between emails. //! // since `Body` holds a pre-encoded body, reusing it means avoiding having //! // to re-encode the same body for every email (this clearly only applies //! // when sending multiple emails with the same attachment). //! let image_body = Body::new(image); //! //! let m = Message::builder() //! .from("NoBody ".parse()?) //! .reply_to("Yuin ".parse()?) //! .to("Hei ".parse()?) //! .subject("Happy new year") //! .multipart( //! MultiPart::mixed() //! .multipart( //! MultiPart::alternative() //! .singlepart(SinglePart::plain(String::from("Hello, world! :)"))) //! .multipart( //! MultiPart::related() //! .singlepart(SinglePart::html(String::from( //! "

Hello, world!

", //! ))) //! .singlepart( //! Attachment::new_inline(String::from("123")) //! .body(image_body, "image/png".parse().unwrap()), //! ), //! ), //! ) //! .singlepart(Attachment::new(String::from("example.rs")).body( //! String::from("fn main() { println!(\"Hello, World!\") }"), //! "text/plain".parse().unwrap(), //! )), //! )?; //! # Ok(()) //! # } //! ``` //! //! Which produces: //!
//! Click to expand //! //! ```sh //! From: NoBody //! Reply-To: Yuin //! To: Hei //! Subject: Happy new year //! MIME-Version: 1.0 //! Date: Sat, 12 Dec 2020 16:30:45 GMT //! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1" //! //! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 //! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk" //! //! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk //! Content-Type: text/plain; charset=utf8 //! Content-Transfer-Encoding: 7bit //! //! Hello, world! :) //! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk //! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr" //! //! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr //! Content-Type: text/html; charset=utf8 //! Content-Transfer-Encoding: 7bit //! //!

Hello, world!

//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr //! Content-Type: image/png //! Content-Disposition: inline //! Content-ID: <123> //! Content-Transfer-Encoding: base64 //! //! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg== //! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr-- //! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk-- //! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 //! Content-Type: text/plain; charset=utf8 //! Content-Disposition: attachment; filename="example.rs" //! Content-Transfer-Encoding: 7bit //! //! fn main() { println!("Hello, World!") } //! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1-- //! ``` //!
use std::{io::Write, iter, time::SystemTime}; pub use attachment::Attachment; pub use body::{Body, IntoBody, MaybeString}; #[cfg(feature = "dkim")] pub use dkim::*; pub use mailbox::*; pub use mimebody::*; mod attachment; mod body; #[cfg(feature = "dkim")] pub mod dkim; pub mod header; mod mailbox; mod mimebody; use crate::{ address::Envelope, message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader}, Error as EmailError, }; const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost"; /// Something that can be formatted as an email message trait EmailFormat { // Use a writer? fn format(&self, out: &mut Vec); } /// A builder for messages #[derive(Debug, Clone)] pub struct MessageBuilder { headers: Headers, envelope: Option, drop_bcc: bool, } impl MessageBuilder { /// Creates a new default message builder pub fn new() -> Self { Self { headers: Headers::new(), envelope: None, drop_bcc: true, } } /// Set or add mailbox to `From` header /// /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2). /// /// Shortcut for `self.mailbox(header::From(mbox))`. pub fn from(self, mbox: Mailbox) -> Self { self.mailbox(header::From::from(Mailboxes::from(mbox))) } /// Set `Sender` header. Should be used when providing several `From` mailboxes. /// /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2). /// /// Shortcut for `self.header(header::Sender(mbox))`. pub fn sender(self, mbox: Mailbox) -> Self { self.header(header::Sender::from(mbox)) } /// Add `Date` header to message /// /// Shortcut for `self.header(header::Date::new(st))`. pub fn date(self, st: SystemTime) -> Self { self.header(header::Date::new(st)) } /// Set `Date` header using current date/time /// /// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted /// if no date has been provided. pub fn date_now(self) -> Self { self.date(SystemTime::now()) } /// Set or add mailbox to `ReplyTo` header /// /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2). /// /// Shortcut for `self.mailbox(header::ReplyTo(mbox))`. pub fn reply_to(self, mbox: Mailbox) -> Self { self.mailbox(header::ReplyTo(mbox.into())) } /// Set or add mailbox to `To` header /// /// Shortcut for `self.mailbox(header::To(mbox))`. pub fn to(self, mbox: Mailbox) -> Self { self.mailbox(header::To(mbox.into())) } /// Set or add mailbox to `Cc` header /// /// Shortcut for `self.mailbox(header::Cc(mbox))`. pub fn cc(self, mbox: Mailbox) -> Self { self.mailbox(header::Cc(mbox.into())) } /// Set or add mailbox to `Bcc` header /// /// Shortcut for `self.mailbox(header::Bcc(mbox))`. pub fn bcc(self, mbox: Mailbox) -> Self { self.mailbox(header::Bcc(mbox.into())) } /// Set or add message id to [`In-Reply-To` /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) pub fn in_reply_to(self, id: String) -> Self { self.header(header::InReplyTo::from(id)) } /// Set or add message id to [`References` /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) pub fn references(self, id: String) -> Self { self.header(header::References::from(id)) } /// Set `Subject` header to message /// /// Shortcut for `self.header(header::Subject(subject.into()))`. pub fn subject>(self, subject: S) -> Self { let s: String = subject.into(); self.header(header::Subject::from(s)) } /// Set [Message-ID /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// /// Should generally be inserted by the mail relay. /// /// If `None` is provided, an id will be generated in the /// ``. pub fn message_id(self, id: Option) -> Self { match id { Some(i) => self.header(header::MessageId::from(i)), None => { #[cfg(feature = "hostname")] let hostname = hostname::get() .map_err(|_| ()) .and_then(|s| s.into_string().map_err(|_| ())) .unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned()); #[cfg(not(feature = "hostname"))] let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned(); self.header(header::MessageId::from( // https://tools.ietf.org/html/rfc5322#section-3.6.4 format!("<{}@{}>", make_message_id(), hostname), )) } } } /// Set [User-Agent /// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00) pub fn user_agent(self, id: String) -> Self { self.header(header::UserAgent::from(id)) } /// Set custom header to message pub fn header(mut self, header: H) -> Self { self.headers.set(header); self } /// Add mailbox to header pub fn mailbox(self, header: H) -> Self { match self.headers.get::() { Some(mut header_) => { header_.join_mailboxes(header); self.header(header_) } None => self.header(header), } } /// Force specific envelope (by default it is derived from headers) pub fn envelope(mut self, envelope: Envelope) -> Self { self.envelope = Some(envelope); self } /// Keep the `Bcc` header /// /// By default, the `Bcc` header is removed from the email after /// using it to generate the message envelope. In some cases though, /// like when saving the email as an `.eml`, or sending through /// some transports (like the Gmail API) that don't take a separate /// envelope value, it becomes necessary to keep the `Bcc` header. /// /// Calling this method overrides the default behavior. pub fn keep_bcc(mut self) -> Self { self.drop_bcc = false; self } // TODO: High-level methods for attachments and embedded files /// Create message from body fn build(self, body: MessageBody) -> Result { // Check for missing required headers // https://tools.ietf.org/html/rfc5322#section-3.6 // Insert Date if missing let mut res = if self.headers.get::().is_none() { self.date_now() } else { self }; // Fail is missing correct originator (Sender or From) match res.headers.get::() { Some(header::From(f)) => { let from: Vec = f.into(); if from.len() > 1 && res.headers.get::().is_none() { return Err(EmailError::TooManyFrom); } } None => { return Err(EmailError::MissingFrom); } } let envelope = match res.envelope { Some(e) => e, None => Envelope::try_from(&res.headers)?, }; if res.drop_bcc { // Remove `Bcc` headers now the envelope is set res.headers.remove::(); } Ok(Message { headers: res.headers, body, envelope, }) } /// Create [`Message`] using a [`Vec`], [`String`], or [`Body`] body /// /// Automatically gets encoded with `7bit`, `quoted-printable` or `base64` /// `Content-Transfer-Encoding`, based on the most efficient and valid encoding /// for `body`. pub fn body(mut self, body: T) -> Result { let maybe_encoding = self.headers.get::(); let body = body.into_body(maybe_encoding); self.headers.set(body.encoding()); self.build(MessageBody::Raw(body.into_vec())) } /// Create message using mime body ([`MultiPart`][self::MultiPart]) pub fn multipart(self, part: MultiPart) -> Result { self.mime_1_0().build(MessageBody::Mime(Part::Multi(part))) } /// Create message using mime body ([`SinglePart`][self::SinglePart]) pub fn singlepart(self, part: SinglePart) -> Result { self.mime_1_0().build(MessageBody::Mime(Part::Single(part))) } /// Set `MIME-Version` header to 1.0 /// /// Shortcut for `self.header(header::MIME_VERSION_1_0)`. /// /// Not exposed as it is set by body methods fn mime_1_0(self) -> Self { self.header(header::MIME_VERSION_1_0) } } /// Email message which can be formatted #[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[derive(Clone, Debug)] pub struct Message { headers: Headers, body: MessageBody, envelope: Envelope, } #[derive(Clone, Debug)] enum MessageBody { Mime(Part), Raw(Vec), } impl Message { /// Create a new message builder without headers pub fn builder() -> MessageBuilder { MessageBuilder::new() } /// Get the headers from the Message pub fn headers(&self) -> &Headers { &self.headers } /// Get a mutable reference to the headers pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } /// Get `Message` envelope pub fn envelope(&self) -> &Envelope { &self.envelope } /// Get message content formatted for SMTP pub fn formatted(&self) -> Vec { let mut out = Vec::new(); self.format(&mut out); out } #[cfg(feature = "dkim")] /// Format body for signing pub(crate) fn body_raw(&self) -> Vec { let mut out = Vec::new(); match &self.body { MessageBody::Mime(p) => p.format_body(&mut out), MessageBody::Raw(r) => out.extend_from_slice(r), }; out.extend_from_slice(b"\r\n"); out } /// Sign the message using Dkim /// /// Example: /// ```rust /// use lettre::{ /// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey}, /// Message, /// }; /// /// let mut message = Message::builder() /// .from("Alice ".parse().unwrap()) /// .reply_to("Bob ".parse().unwrap()) /// .to("Carla ".parse().unwrap()) /// .subject("Hello") /// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned()) /// .unwrap(); /// let key = "-----BEGIN RSA PRIVATE KEY----- /// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8 /// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL /// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4 /// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA /// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU /// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo /// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc /// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM /// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX /// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR /// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L /// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg /// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK /// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F /// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN /// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q /// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x /// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW /// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu /// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/ /// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo /// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI /// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX /// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw /// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25 /// -----END RSA PRIVATE KEY-----"; /// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap(); /// message.sign(&DkimConfig::default_config( /// "dkimtest".to_owned(), /// "example.org".to_owned(), /// signing_key, /// )); /// println!( /// "message: {}", /// std::str::from_utf8(&message.formatted()).unwrap() /// ); /// ``` #[cfg(feature = "dkim")] pub fn sign(&mut self, dkim_config: &DkimConfig) { dkim_sign(self, dkim_config); } } impl EmailFormat for Message { fn format(&self, out: &mut Vec) { write!(out, "{}", self.headers) .expect("A Write implementation panicked while formatting headers"); match &self.body { MessageBody::Mime(p) => p.format(out), MessageBody::Raw(r) => { out.extend_from_slice(b"\r\n"); out.extend_from_slice(r) } } } } impl Default for MessageBuilder { fn default() -> Self { MessageBuilder::new() } } /// Create a random message id. /// (Not cryptographically random) fn make_message_id() -> String { iter::repeat_with(fastrand::alphanumeric).take(36).collect() } #[cfg(test)] mod test { use std::time::{Duration, SystemTime}; use pretty_assertions::assert_eq; use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart}; #[test] fn email_missing_originator() { assert!(Message::builder() .body(String::from("Happy new year!")) .is_err()); } #[test] fn email_minimal_message() { assert!(Message::builder() .from("NoBody ".parse().unwrap()) .to("NoBody ".parse().unwrap()) .body(String::from("Happy new year!")) .is_ok()); } #[test] fn email_missing_sender() { assert!(Message::builder() .from("NoBody ".parse().unwrap()) .from("AnyBody ".parse().unwrap()) .body(String::from("Happy new year!")) .is_err()); } #[test] fn email_message_no_bcc() { // Tue, 15 Nov 1994 08:12:31 GMT let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151); let email = Message::builder() .date(date) .bcc("hidden@example.com".parse().unwrap()) .header(header::From( vec![Mailbox::new( Some("Каи".into()), "kayo@example.com".parse().unwrap(), )] .into(), )) .header(header::To( vec!["Pony O.P. ".parse().unwrap()].into(), )) .header(header::Subject::from(String::from("яңа ел белән!"))) .body(String::from("Happy new year!")) .unwrap(); assert_eq!( String::from_utf8(email.formatted()).unwrap(), concat!( "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n", "From: =?utf-8?b?0JrQsNC4?= \r\n", "To: \"Pony O.P.\" \r\n", "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Happy new year!" ) ); } #[test] fn email_message_keep_bcc() { // Tue, 15 Nov 1994 08:12:31 GMT let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151); let email = Message::builder() .date(date) .bcc("hidden@example.com".parse().unwrap()) .keep_bcc() .header(header::From( vec![Mailbox::new( Some("Каи".into()), "kayo@example.com".parse().unwrap(), )] .into(), )) .header(header::To( vec!["Pony O.P. ".parse().unwrap()].into(), )) .header(header::Subject::from(String::from("яңа ел белән!"))) .body(String::from("Happy new year!")) .unwrap(); assert_eq!( String::from_utf8(email.formatted()).unwrap(), concat!( "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n", "Bcc: hidden@example.com\r\n", "From: =?utf-8?b?0JrQsNC4?= \r\n", "To: \"Pony O.P.\" \r\n", "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Happy new year!" ) ); } #[test] fn email_with_png() { // Tue, 15 Nov 1994 08:12:31 GMT let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151); let img = std::fs::read("./docs/lettre.png").unwrap(); let m = Message::builder() .date(date) .from("NoBody ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap()) .to("Hei ".parse().unwrap()) .subject("Happy new year") .multipart( MultiPart::related() .singlepart( SinglePart::builder() .header(header::ContentType::TEXT_HTML) .body(String::from( "

Hello, world!

", )), ) .singlepart( SinglePart::builder() .header(header::ContentType::parse("image/png").unwrap()) .header(header::ContentDisposition::inline()) .header(header::ContentId::from(String::from("<123>"))) .body(img), ), ) .unwrap(); let output = String::from_utf8(m.formatted()).unwrap(); let file_expected = std::fs::read("./testdata/email_with_png.eml").unwrap(); let expected = String::from_utf8(file_expected).unwrap(); for (i, line) in output.lines().zip(expected.lines()).enumerate() { if i == 7 || i == 9 || i == 14 || i == 233 { continue; } assert_eq!(line.0, line.1) } } #[test] fn test_make_message_id() { let mut ids = std::collections::HashSet::with_capacity(10); for _ in 0..1000 { ids.insert(make_message_id()); } // Ensure there are no duplicates assert_eq!(1000, ids.len()); // Ensure correct length for id in ids { assert_eq!(36, id.len()); } } }