use async_trait::async_trait;
use lettre::{
message::{
header::ContentType, Attachment as LettreAttachment, Mailbox, MultiPart, SinglePart,
},
Message,
};
use reqwest::Client;
use serde::Deserialize;
use crate::address::Address;
use crate::attachment::AttachmentType;
use crate::email::Email;
use crate::error::MailError;
use crate::mailer::{DeliveryResult, Mailer};
const GMAIL_API_URL: &str =
"https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media";
pub struct GmailMailer {
access_token: String,
client: Client,
base_url: String,
}
impl GmailMailer {
pub fn new(access_token: impl Into<String>) -> Self {
Self {
access_token: access_token.into(),
client: Client::new(),
base_url: GMAIL_API_URL.to_string(),
}
}
pub fn with_client(access_token: impl Into<String>, client: Client) -> Self {
Self {
access_token: access_token.into(),
client,
base_url: GMAIL_API_URL.to_string(),
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
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 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 GmailMailer {
async fn deliver(&self, email: &Email) -> Result<DeliveryResult, MailError> {
let message = self.build_message(email)?;
let raw_message = message.formatted();
let response = self
.client
.post(&self.base_url)
.header("Content-Type", "message/rfc822")
.header("Authorization", format!("Bearer {}", self.access_token))
.header("User-Agent", format!("missive/{}", crate::VERSION))
.body(raw_message)
.send()
.await?;
let status = response.status();
if status.is_success() {
let result: GmailResponse = response.json().await?;
Ok(DeliveryResult::with_response(
result.id.clone(),
serde_json::json!({
"provider": "gmail",
"id": result.id,
"threadId": result.thread_id,
"labelIds": result.label_ids,
}),
))
} else {
let error_text = response.text().await.unwrap_or_default();
Err(MailError::provider_with_status(
"gmail",
error_text,
status.as_u16(),
))
}
}
fn provider_name(&self) -> &'static str {
"gmail"
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GmailResponse {
id: String,
thread_id: String,
#[serde(default)]
label_ids: Vec<String>,
}
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))
}