use async_trait::async_trait;
use lettre::{
message::{
header::ContentType, Attachment as LettreAttachment, Mailbox, MultiPart, SinglePart,
},
transport::smtp::authentication::Credentials,
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
};
use crate::address::Address;
use crate::attachment::AttachmentType;
use crate::email::Email;
use crate::error::MailError;
use crate::mailer::{DeliveryResult, Mailer};
pub struct SmtpMailer {
transport: AsyncSmtpTransport<Tokio1Executor>,
}
impl SmtpMailer {
#[allow(clippy::new_ret_no_self)]
pub fn new(host: &str, port: u16) -> SmtpBuilder {
SmtpBuilder {
host: host.to_string(),
port,
credentials: None,
tls: TlsMode::StartTls,
}
}
pub fn localhost() -> Self {
let transport = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("localhost")
.port(25)
.build();
Self { transport }
}
fn build_message(&self, email: &Email) -> Result<Message, MailError> {
let from = email.from.as_ref().ok_or(MailError::MissingField("from"))?;
if email.to.is_empty() {
return Err(MailError::MissingField("to"));
}
let mut builder = Message::builder()
.from(address_to_mailbox(from)?)
.subject(&email.subject);
for to in &email.to {
builder = builder.to(address_to_mailbox(to)?);
}
for cc in &email.cc {
builder = builder.cc(address_to_mailbox(cc)?);
}
for bcc in &email.bcc {
builder = builder.bcc(address_to_mailbox(bcc)?);
}
if let Some(reply_to) = email.reply_to.first() {
builder = builder.reply_to(address_to_mailbox(reply_to)?);
}
let _ = &email.headers;
let message = if email.attachments.is_empty() {
match (&email.html_body, &email.text_body) {
(Some(html), Some(text)) => builder.multipart(
MultiPart::alternative_plain_html(text.clone(), html.clone()),
)?,
(Some(html), None) => builder.header(ContentType::TEXT_HTML).body(html.clone())?,
(None, Some(text)) => builder.header(ContentType::TEXT_PLAIN).body(text.clone())?,
(None, None) => builder
.header(ContentType::TEXT_PLAIN)
.body(String::new())?,
}
} else {
let body_part = match (&email.html_body, &email.text_body) {
(Some(html), Some(text)) => {
MultiPart::alternative_plain_html(text.clone(), html.clone())
}
(Some(html), None) => MultiPart::mixed().singlepart(
SinglePart::builder()
.header(ContentType::TEXT_HTML)
.body(html.clone()),
),
(None, Some(text)) => MultiPart::mixed().singlepart(
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(text.clone()),
),
(None, None) => MultiPart::mixed().singlepart(
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(String::new()),
),
};
let mut multipart = MultiPart::mixed().multipart(body_part);
for attachment in &email.attachments {
let content_type: ContentType = attachment
.content_type
.parse()
.unwrap_or(ContentType::TEXT_PLAIN);
let lettre_attachment = match attachment.disposition {
AttachmentType::Inline => {
let cid = attachment
.content_id
.as_ref()
.unwrap_or(&attachment.filename);
LettreAttachment::new_inline(cid.clone())
.body(attachment.data.clone(), content_type)
}
AttachmentType::Attachment => {
LettreAttachment::new(attachment.filename.clone())
.body(attachment.data.clone(), content_type)
}
};
multipart = multipart.singlepart(lettre_attachment);
}
builder.multipart(multipart)?
};
Ok(message)
}
}
#[async_trait]
impl Mailer for SmtpMailer {
async fn deliver(&self, email: &Email) -> Result<DeliveryResult, MailError> {
let message = self.build_message(email)?;
let response = self
.transport
.send(message)
.await
.map_err(|e| MailError::SendError(e.to_string()))?;
let message_id = response
.message()
.next()
.and_then(|m| m.lines().next())
.map(|s| s.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
Ok(DeliveryResult::new(message_id))
}
fn provider_name(&self) -> &'static str {
"smtp"
}
}
#[derive(Debug, Clone, Copy)]
pub enum TlsMode {
None,
StartTls,
Tls,
}
pub struct SmtpBuilder {
host: String,
port: u16,
credentials: Option<Credentials>,
tls: TlsMode,
}
impl SmtpBuilder {
pub fn credentials(mut self, username: &str, password: &str) -> Self {
self.credentials = Some(Credentials::new(username.to_string(), password.to_string()));
self
}
pub fn tls(mut self, mode: TlsMode) -> Self {
self.tls = mode;
self
}
pub fn no_tls(mut self) -> Self {
self.tls = TlsMode::None;
self
}
pub fn build(self) -> SmtpMailer {
let transport = match self.tls {
TlsMode::None => {
let mut t = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&self.host)
.port(self.port);
if let Some(creds) = self.credentials {
t = t.credentials(creds);
}
t.build()
}
TlsMode::StartTls => {
let mut t = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&self.host)
.unwrap_or_else(|_| {
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&self.host)
})
.port(self.port);
if let Some(creds) = self.credentials {
t = t.credentials(creds);
}
t.build()
}
TlsMode::Tls => {
let mut t = AsyncSmtpTransport::<Tokio1Executor>::relay(&self.host)
.unwrap_or_else(|_| {
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&self.host)
})
.port(self.port);
if let Some(creds) = self.credentials {
t = t.credentials(creds);
}
t.build()
}
};
SmtpMailer { transport }
}
}
fn address_to_mailbox(addr: &Address) -> Result<Mailbox, MailError> {
let email = addr
.email
.parse()
.map_err(|e: lettre::address::AddressError| MailError::InvalidAddress(e.to_string()))?;
Ok(Mailbox::new(addr.name.clone(), email))
}