use crate::{error::Error as LettreError, Email, EmailAddress, Envelope}; pub use email::{Address, Header, Mailbox as OriginalMailbox, MimeMessage, MimeMultipartType}; use error::Error; pub use mime; use mime::Mime; use std::borrow::Cow; use std::ffi::OsStr; use std::fmt; use std::fs; use std::path::Path; use std::str::FromStr; use time::OffsetDateTime; use uuid::Uuid; pub mod error; const DT_RFC822Z: &str = "%a, %d %b %Y %T %z"; // From rust-email, allows adding rfc2047 encoding /// Represents an RFC 5322 mailbox #[derive(PartialEq, Eq, Debug, Clone)] pub struct Mailbox { inner: OriginalMailbox, } impl Mailbox { /// Create a new Mailbox without a display name pub fn new(address: String) -> Mailbox { Mailbox { inner: OriginalMailbox::new(address), } } /// Create a new Mailbox with a display name pub fn new_with_name(name: String, address: String) -> Mailbox { Mailbox { inner: OriginalMailbox::new_with_name(encode_rfc2047(&name).to_string(), address), } } fn original(self) -> OriginalMailbox { self.inner } } impl fmt::Display for Mailbox { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "{}", self.inner) } } impl<'a> From<&'a str> for Mailbox { fn from(mailbox: &'a str) -> Mailbox { Mailbox::new(mailbox.into()) } } impl From for Mailbox { fn from(mailbox: String) -> Mailbox { Mailbox::new(mailbox) } } impl, T: Into> From<(S, T)> for Mailbox { fn from(header: (S, T)) -> Mailbox { let (address, alias) = header; Mailbox::new_with_name(alias.into(), address.into()) } } /// Encode a UTF-8 string according to RFC 2047, if need be. /// /// Currently, this only uses "B" encoding, when pure ASCII cannot represent the /// string accurately. /// /// Can be used on header content. pub fn encode_rfc2047(text: &str) -> Cow { if text.is_ascii() { Cow::Borrowed(text) } else { Cow::Owned( base64::encode_config(text.as_bytes(), base64::STANDARD) // base64 so ascii .as_bytes() // Max length - wrapping chars .chunks(75 - 12) .map(|d| format!("=?utf-8?B?{}?=", std::str::from_utf8(d).unwrap())) .collect::>() .join("\r\n"), ) } } impl From for OriginalMailbox { fn from(addr: EmailAddress) -> Self { OriginalMailbox::new(addr.into_inner()) } } /// Builds a `MimeMessage` structure #[derive(PartialEq, Eq, Clone, Debug)] pub struct PartBuilder { /// Message message: MimeMessage, } impl Default for PartBuilder { fn default() -> Self { Self::new() } } /// Represents a message id pub type MessageId = String; /// Builds an `Email` structure #[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct EmailBuilder { /// Message message: PartBuilder, /// The recipients' addresses for the mail header to: Vec
, /// The sender addresses for the mail header from: Vec
, /// The Cc addresses for the mail header cc: Vec
, /// The Bcc addresses for the mail header bcc: Vec
, /// The Reply-To addresses for the mail header reply_to: Vec
, /// The In-Reply-To ids for the mail header in_reply_to: Vec, /// The References ids for the mail header references: Vec, /// The sender address for the mail header sender: Option, /// The envelope envelope: Option, /// Date issued date_issued: bool, /// Message-ID message_id: Option, } impl PartBuilder { /// Creates a new empty part pub fn new() -> PartBuilder { PartBuilder { message: MimeMessage::new_blank_message(), } } /// Adds a generic header pub fn header>(mut self, header: A) -> PartBuilder { self.message.headers.insert(header.into()); self } /// Sets the body pub fn body>(mut self, body: S) -> PartBuilder { self.message.body = body.into(); self } /// Defines a `MimeMultipartType` value pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder { self.message.message_type = Some(mime_type); self } /// Adds a `ContentType` header with the given MIME type pub fn content_type(self, content_type: &Mime) -> PartBuilder { self.header(("Content-Type", content_type.to_string())) } /// Adds a child part pub fn child(mut self, child: MimeMessage) -> PartBuilder { self.message.children.push(child); self } /// Gets built `MimeMessage` pub fn build(mut self) -> MimeMessage { self.message.update_headers(); self.message } } impl EmailBuilder { /// Creates a new empty email pub fn new() -> EmailBuilder { EmailBuilder { message: PartBuilder::new(), to: vec![], from: vec![], cc: vec![], bcc: vec![], reply_to: vec![], in_reply_to: vec![], references: vec![], sender: None, envelope: None, date_issued: false, message_id: None, } } /// Sets the email body pub fn body>(mut self, body: S) -> EmailBuilder { self.message = self.message.body(body); self } /// Add a generic header pub fn header>(mut self, header: A) -> EmailBuilder { self.message = self.message.header(header); self } /// Adds a `From` header and stores the sender address pub fn from>(mut self, address: A) -> EmailBuilder { let mailbox = address.into(); self.from.push(Address::Mailbox(mailbox.original())); self } /// Adds a `To` header and stores the recipient address pub fn to>(mut self, address: A) -> EmailBuilder { let mailbox = address.into(); self.to.push(Address::Mailbox(mailbox.original())); self } /// Adds a `Cc` header and stores the recipient address pub fn cc>(mut self, address: A) -> EmailBuilder { let mailbox = address.into(); self.cc.push(Address::Mailbox(mailbox.original())); self } /// Adds a `Bcc` header and stores the recipient address pub fn bcc>(mut self, address: A) -> EmailBuilder { let mailbox = address.into(); self.bcc.push(Address::Mailbox(mailbox.original())); self } /// Adds a `Reply-To` header pub fn reply_to>(mut self, address: A) -> EmailBuilder { let mailbox = address.into(); self.reply_to.push(Address::Mailbox(mailbox.original())); self } /// Adds a `In-Reply-To` header pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder { self.in_reply_to.push(message_id); self } /// Adds a `References` header pub fn references(mut self, message_id: MessageId) -> EmailBuilder { self.references.push(message_id); self } /// Adds a `Sender` header pub fn sender>(mut self, address: A) -> EmailBuilder { let mailbox = address.into(); self.sender = Some(mailbox.original()); self } /// Adds a `Subject` header pub fn subject>(mut self, subject: S) -> EmailBuilder { self.message = self.message.header(( "Subject".to_string(), encode_rfc2047(subject.into().as_ref()), )); self } /// Adds a `Date` header with the given date pub fn date(mut self, date: &OffsetDateTime) -> EmailBuilder { self.message = self.message.header(("Date", date.format(DT_RFC822Z))); self.date_issued = true; self } /// Adds an attachment to the email from a file /// /// If not specified, the filename will be extracted from the file path. pub fn attachment_from_file( self, path: &Path, filename: Option<&str>, content_type: &Mime, ) -> Result { self.attachment( fs::read(path)?.as_slice(), filename.unwrap_or( path.file_name() .and_then(OsStr::to_str) .ok_or(Error::CannotParseFilename)?, ), content_type, ) } /// Adds an attachment to the email from a vector of bytes. pub fn attachment( self, body: &[u8], filename: &str, content_type: &Mime, ) -> Result { let encoded_body = base64::encode(&body); let content = PartBuilder::new() .body(encoded_body) .header(( "Content-Disposition", format!("attachment; filename=\"{}\"", filename), )) .header(("Content-Type", content_type.to_string())) .header(("Content-Transfer-Encoding", "base64")) .build(); Ok(self.message_type(MimeMultipartType::Mixed).child(content)) } /// Set the message type pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder { self.message = self.message.message_type(message_type); self } /// Adds a child pub fn child(mut self, child: MimeMessage) -> EmailBuilder { self.message = self.message.child(child); self } /// Sets the email body to plain text content pub fn text>(self, body: S) -> EmailBuilder { let text = PartBuilder::new() .body(body) .header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string())) .build(); self.child(text) } /// Sets the email body to HTML content pub fn html>(self, body: S) -> EmailBuilder { let html = PartBuilder::new() .body(body) .header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string())) .build(); self.child(html) } /// Sets the email content pub fn alternative, T: Into>( self, body_html: S, body_text: T, ) -> EmailBuilder { let text = PartBuilder::new() .body(body_text) .header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string())) .build(); let html = PartBuilder::new() .body(body_html) .header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string())) .build(); let alternate = PartBuilder::new() .message_type(MimeMultipartType::Alternative) .child(text) .child(html); self.message_type(MimeMultipartType::Mixed) .child(alternate.build()) } /// Sets the `Message-ID` header pub fn message_id>(mut self, id: S) -> EmailBuilder { self.message = self.message.header(("Message-ID", id.clone())); self.message_id = Some(id.into()); self } /// Sets the envelope for manual destination control /// If this function is not called, the envelope will be calculated /// from the "to" and "cc" addresses you set. pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder { self.envelope = Some(envelope); self } /// Only builds the body, this can be used to encrypt or sign /// using S/MIME pub fn build_body(self) -> Result, Error> { Ok(self.message.build().as_string().into_bytes()) } /// Builds the Email pub fn build(mut self) -> Result { // If there are multiple addresses in "From", the "Sender" is required. if self.from.len() >= 2 && self.sender.is_none() { // So, we must find something to put as Sender. for possible_sender in &self.from { // Only a mailbox can be used as sender, not Address::Group. if let Address::Mailbox(ref mbx) = *possible_sender { self.sender = Some(mbx.clone()); break; } } // Address::Group is not yet supported, so the line below will never panic. // If groups are supported one day, add another Error for this case // and return it here, if sender_header is still None at this point. assert!(self.sender.is_some()); } // Add the sender header, if any. if let Some(ref v) = self.sender { self.message = self.message.header(("Sender", v.to_string())); } // Calculate the envelope let envelope = match self.envelope { Some(e) => e, None => { // we need to generate the envelope let mut to = vec![]; // add all receivers in to_header and cc_header for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) { match *receiver { Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?), Address::Group(_, ref ms) => { for m in ms.iter() { to.push(EmailAddress::from_str(&m.address.clone())?); } } } } let from = Some(EmailAddress::from_str(&match self.sender { Some(x) => Ok(x.address), // if we have a sender_header, use it None => { // use a from header debug_assert!(self.from.len() <= 1); // else we'd have sender_header match self.from.first() { Some(a) => match *a { // if we have a from header Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it Address::Group(_, ref mailbox_list) => match mailbox_list.first() { // if it's an author group, use the first author Some(mailbox) => Ok(mailbox.address.clone()), // for an empty author group (the rarest of the rare cases) None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender }, }, // if we don't have a from header None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender } } }?)?); Envelope::new(from, to)? } }; // Add the collected addresses as mailbox-list all at once. // The unwraps are fine because the conversions for Vec
never errs. if !self.to.is_empty() { self.message = self .message .header(Header::new_with_value("To".into(), self.to).unwrap()); } if !self.from.is_empty() { self.message = self .message .header(Header::new_with_value("From".into(), self.from).unwrap()); } else if let Some(from) = envelope.from() { let from = vec![Address::new_mailbox(from.to_string())]; self.message = self .message .header(Header::new_with_value("From".into(), from).unwrap()); } else { return Err(Error::Envelope(LettreError::MissingFrom)); } if !self.cc.is_empty() { self.message = self .message .header(Header::new_with_value("Cc".into(), self.cc).unwrap()); } if !self.reply_to.is_empty() { self.message = self .message .header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap()); } if !self.in_reply_to.is_empty() { self.message = self.message.header( Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(), ); } if !self.references.is_empty() { self.message = self.message.header( Header::new_with_value("References".into(), self.references.join(" ")).unwrap(), ); } if !self.date_issued { self.message = self .message .header(("Date", OffsetDateTime::now().format(DT_RFC822Z))); } self.message = self.message.header(("MIME-Version", "1.0")); let message_id = match self.message_id { Some(id) => id, None => { let message_id = Uuid::new_v4(); self.message = self .message .header(("Message-ID", format!("<{}.lettre@localhost>", message_id))); message_id.to_string() } }; Ok(Email::new( envelope, message_id, self.message.build().as_string().into_bytes(), )) } } #[cfg(test)] mod test { use super::*; use crate::EmailAddress; use time::OffsetDateTime; #[test] fn test_encode_rfc2047() { assert_eq!(encode_rfc2047("test"), "test"); assert_eq!(encode_rfc2047("testà"), "=?utf-8?B?dGVzdMOg?="); assert_eq!( encode_rfc2047( "testàtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest" ), "=?utf-8?B?dGVzdMOgdGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHR?=\r\n=?utf-8?B?lc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0?=" ); } #[test] fn test_multiple_from() { let email_builder = EmailBuilder::new(); let date_now = OffsetDateTime::now(); let email: Email = email_builder .to("anna@example.com") .from("dieter@example.com") .from("joachim@example.com") .date(&date_now) .subject("Invitation") .body("We invite you!") .build() .unwrap() .into(); let id = email.message_id().to_string(); assert_eq!( email.message_to_string().unwrap(), format!( "Date: {}\r\nSubject: Invitation\r\nSender: \ \r\nTo: \r\nFrom: \ , \r\nMIME-Version: \ 1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n", date_now.format(DT_RFC822Z), id ) ); } #[test] fn test_email_builder() { let email_builder = EmailBuilder::new(); let date_now = OffsetDateTime::now(); let email: Email = email_builder .to("user@localhost") .from("user@localhost") .cc(("cc@localhost", "Alias")) .cc(("cc2@localhost", "Aliäs")) .bcc("bcc@localhost") .reply_to("reply@localhost") .in_reply_to("original".to_string()) .sender("sender@localhost") .body("Hello World!") .date(&date_now) .subject("Hello") .header(("X-test", "value")) .build() .unwrap() .into(); let id = email.message_id().to_string(); assert_eq!( email.message_to_string().unwrap(), format!( "Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \ \r\nTo: \r\nFrom: \ \r\nCc: \"Alias\" , \"=?utf-8?B?QWxpw6Rz?=\" \r\n\ Reply-To: \r\nIn-Reply-To: original\r\n\ MIME-Version: 1.0\r\nMessage-ID: \ <{}.lettre@localhost>\r\n\r\nHello World!\r\n", date_now.format(DT_RFC822Z), id ) ); } #[test] fn test_custom_message_id() { let email_builder = EmailBuilder::new(); let date_now = OffsetDateTime::now(); let email: Email = email_builder .to("user@localhost") .from("user@localhost") .cc(("cc@localhost", "Alias")) .bcc("bcc@localhost") .reply_to("reply@localhost") .in_reply_to("original".to_string()) .sender("sender@localhost") .body("Hello World!") .date(&date_now) .subject("Hello") .header(("X-test", "value")) .message_id("my-shiny-id") .build() .unwrap() .into(); assert_eq!( email.message_to_string().unwrap(), format!( "Date: {}\r\nSubject: Hello\r\nX-test: value\r\nMessage-ID: \ my-shiny-id\r\nSender: \r\nTo: \r\nFrom: \ \r\nCc: \"Alias\" \r\nReply-To: \ \r\nIn-Reply-To: original\r\nMIME-Version: 1.0\r\n\r\nHello \ World!\r\n", date_now.format(DT_RFC822Z) ) ); } #[test] fn test_email_builder_body() { let date_now = OffsetDateTime::now(); let email_builder = EmailBuilder::new() .text("TestTest") .subject("A Subject") .to("user@localhost") .date(&date_now); let string_res = String::from_utf8(email_builder.build_body().unwrap()); assert!(string_res.unwrap().starts_with("Subject: A Subject\r\n")); } #[test] fn test_email_subject_encoding() { let date_now = OffsetDateTime::now(); let email_builder = EmailBuilder::new() .text("TestTest") .subject("A ö Subject") .to("user@localhost") .date(&date_now); let string_res = String::from_utf8(email_builder.build_body().unwrap()); assert!(string_res .unwrap() .starts_with("Subject: =?utf-8?B?QSDDtiBTdWJqZWN0?=\r\n")); } #[test] fn test_email_sendable() { let email_builder = EmailBuilder::new(); let date_now = OffsetDateTime::now(); let email: Email = email_builder .to("user@localhost") .from("user@localhost") .cc(("cc@localhost", "Alias")) .bcc("bcc@localhost") .reply_to("reply@localhost") .sender("sender@localhost") .body("Hello World!") .date(&date_now) .subject("Hello") .header(("X-test", "value")) .build() .unwrap() .into(); assert_eq!( email.envelope().from().unwrap().to_string(), "sender@localhost".to_string() ); assert_eq!( email.envelope().to(), vec![ EmailAddress::new("user@localhost".to_string()).unwrap(), EmailAddress::new("cc@localhost".to_string()).unwrap(), EmailAddress::new("bcc@localhost".to_string()).unwrap(), ] .as_slice() ); } }