#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.2")]
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
extern crate base64;
extern crate email as email_format;
extern crate lettre;
extern crate time;
extern crate uuid;
pub extern crate mime;
pub mod error;
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
use error::Error;
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
use mime::Mime;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use time::{now, Tm};
use uuid::Uuid;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct PartBuilder {
message: MimeMessage,
}
impl Default for PartBuilder {
fn default() -> Self {
Self::new()
}
}
pub type MessageId = String;
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EmailBuilder {
message: PartBuilder,
to: Vec<Address>,
from: Vec<Address>,
cc: Vec<Address>,
bcc: Vec<Address>,
reply_to: Vec<Address>,
in_reply_to: Vec<MessageId>,
references: Vec<MessageId>,
sender: Option<Mailbox>,
envelope: Option<Envelope>,
date_issued: bool,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Email {
message: Vec<u8>,
envelope: Envelope,
message_id: Uuid,
}
impl Into<SendableEmail> for Email {
fn into(self) -> SendableEmail {
SendableEmail::new(
self.envelope.clone(),
self.message_id.to_string(),
self.message,
)
}
}
impl Email {
pub fn builder() -> EmailBuilder {
EmailBuilder::new()
}
}
impl PartBuilder {
pub fn new() -> PartBuilder {
PartBuilder {
message: MimeMessage::new_blank_message(),
}
}
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
self.message.headers.insert(header.into());
self
}
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
self.message.body = body.into();
self
}
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
self.message.message_type = Some(mime_type);
self
}
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
self.header(("Content-Type", content_type.to_string()))
}
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
self.message.children.push(child);
self
}
pub fn build(mut self) -> MimeMessage {
self.message.update_headers();
self.message
}
}
impl EmailBuilder {
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,
}
}
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.message = self.message.body(body);
self
}
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
self.message = self.message.header(header);
self
}
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.from.push(Address::Mailbox(mailbox));
self
}
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.to.push(Address::Mailbox(mailbox));
self
}
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.cc.push(Address::Mailbox(mailbox));
self
}
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.bcc.push(Address::Mailbox(mailbox));
self
}
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.reply_to.push(Address::Mailbox(mailbox));
self
}
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
self.in_reply_to.push(message_id);
self
}
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
self.references.push(message_id);
self
}
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.sender = Some(mailbox);
self
}
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
self.message = self.message.header(("Subject".to_string(), subject.into()));
self
}
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.message = self.message.header(("Date", Tm::rfc822z(date).to_string()));
self.date_issued = true;
self
}
pub fn attachment_from_file(
self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
self.attachment(
fs::read(path)?.as_slice(),
filename.unwrap_or(
path.file_name()
.and_then(|x| x.to_str())
.ok_or(Error::CannotParseFilename)?,
),
content_type,
)
}
pub fn attachment(
self,
body: &[u8],
filename: &str,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
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))
}
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
self.message = self.message.message_type(message_type);
self
}
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
self.message = self.message.child(child);
self
}
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
let text = PartBuilder::new()
.body(body)
.header((
"Content-Type",
mime::TEXT_PLAIN_UTF_8.to_string(),
))
.build();
self.child(text)
}
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
let html = PartBuilder::new()
.body(body)
.header((
"Content-Type",
mime::TEXT_HTML_UTF_8.to_string(),
))
.build();
self.child(html)
}
pub fn alternative<S: Into<String>, T: Into<String>>(
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())
}
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
self.envelope = Some(envelope);
self
}
pub fn build(mut self) -> Result<Email, Error> {
if self.from.len() >= 2 && self.sender.is_none() {
for possible_sender in &self.from {
if let Address::Mailbox(ref mbx) = *possible_sender {
self.sender = Some(mbx.clone());
break;
}
}
assert!(self.sender.is_some());
}
if let Some(ref v) = self.sender {
self.message = self.message.header(("Sender", v.to_string()));
}
let envelope = match self.envelope {
Some(e) => e,
None => {
let mut to = vec![];
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.clone()),
None => {
debug_assert!(self.from.len() <= 1);
match self.from.first() {
Some(a) => match *a {
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()),
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
Some(mailbox) => Ok(mailbox.address.clone()),
None => Err(Error::Envelope(
LettreError::MissingFrom,
)),
},
},
None => Err(Error::Envelope(
LettreError::MissingFrom,
)),
}
}
}?)?);
Envelope::new(from, to)?
}
};
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 {
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", Tm::rfc822z(&now()).to_string()));
}
self.message = self.message.header(("MIME-Version", "1.0"));
let message_id = Uuid::new_v4();
if let Ok(header) = Header::new_with_value(
"Message-ID".to_string(),
format!("<{}.lettre@localhost>", message_id),
) {
self.message = self.message.header(header)
}
Ok(Email {
message: self.message.build().as_string().into_bytes(),
envelope,
message_id,
})
}
}
#[cfg(test)]
mod test {
use super::{EmailBuilder, SendableEmail};
use lettre::EmailAddress;
use time::now;
#[test]
fn test_multiple_from() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = 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: \
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
date_now.rfc822z(),
id
)
);
}
#[test]
fn test_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = 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"))
.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: \
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
Reply-To: <reply@localhost>\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.rfc822z(),
id
)
);
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = 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()
);
}
}