use async_trait::async_trait;
use reqwest::{Client, multipart::Form};
use tracing::debug;
use crate::{Email, MailError, Result, Transport};
#[derive(Debug, Clone)]
pub struct MailgunConfig {
pub api_key: String,
pub domain: String,
pub region: MailgunRegion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MailgunRegion {
Us,
Eu,
}
impl Default for MailgunRegion {
fn default() -> Self {
Self::Us
}
}
impl MailgunConfig {
pub fn new(api_key: impl Into<String>, domain: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
domain: domain.into(),
region: MailgunRegion::Us,
}
}
pub fn region(mut self, region: MailgunRegion) -> Self {
self.region = region;
self
}
pub fn eu(mut self) -> Self {
self.region = MailgunRegion::Eu;
self
}
fn endpoint(&self) -> String {
let base = match self.region {
MailgunRegion::Us => "https://api.mailgun.net",
MailgunRegion::Eu => "https://api.eu.mailgun.net",
};
format!("{}/v3/{}/messages", base, self.domain)
}
}
pub struct MailgunTransport {
client: Client,
config: MailgunConfig,
}
impl MailgunTransport {
pub fn new(config: MailgunConfig) -> Self {
Self {
client: Client::new(),
config,
}
}
}
#[async_trait]
impl Transport for MailgunTransport {
async fn send(&self, email: &Email) -> Result<()> {
email.validate()?;
let from = email
.from
.as_ref()
.ok_or(MailError::MissingField("from"))?
.to_string();
debug!(
to = ?email.to.iter().map(|a| &a.email).collect::<Vec<_>>(),
subject = ?email.subject,
"Sending email via Mailgun"
);
let mut form = Form::new()
.text("from", from)
.text("subject", email.subject.clone().unwrap_or_default());
for addr in &email.to {
form = form.text("to", addr.to_string());
}
for addr in &email.cc {
form = form.text("cc", addr.to_string());
}
for addr in &email.bcc {
form = form.text("bcc", addr.to_string());
}
if let Some(text) = &email.text {
form = form.text("text", text.clone());
}
if let Some(html) = &email.html {
form = form.text("html", html.clone());
}
if let Some(reply_to) = &email.reply_to {
form = form.text("h:Reply-To", reply_to.to_string());
}
for attachment in &email.attachments {
let part = reqwest::multipart::Part::bytes(attachment.data.clone())
.file_name(attachment.filename.clone())
.mime_str(&attachment.content_type)
.map_err(|e| MailError::Attachment(e.to_string()))?;
form = if attachment.content_id.is_some() {
form.part("inline", part)
} else {
form.part("attachment", part)
};
}
let response = self
.client
.post(&self.config.endpoint())
.basic_auth("api", Some(&self.config.api_key))
.multipart(form)
.send()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
let status = response.status();
if status.is_success() {
debug!("Email sent successfully via Mailgun");
Ok(())
} else if status.as_u16() == 429 {
Err(MailError::RateLimited(60))
} else {
let body = response.text().await.unwrap_or_default();
Err(MailError::Provider(format!(
"Mailgun error {}: {}",
status, body
)))
}
}
}