use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::email::Email;
use crate::error::MailError;
use crate::mailer::{DeliveryResult, Mailer};
const MAILTRAP_BASE_URL: &str = "https://send.api.mailtrap.io";
const MAILTRAP_SANDBOX_BASE_URL: &str = "https://sandbox.api.mailtrap.io";
const MAILTRAP_API_ENDPOINT: &str = "/api/send";
pub struct MailtrapMailer {
api_key: String,
base_url: Option<String>,
sandbox_inbox_id: Option<String>,
client: Client,
}
impl MailtrapMailer {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: None,
sandbox_inbox_id: None,
client: Client::new(),
}
}
pub fn with_client(api_key: impl Into<String>, client: Client) -> Self {
Self {
api_key: api_key.into(),
base_url: None,
sandbox_inbox_id: None,
client,
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
pub fn sandbox_inbox_id(mut self, inbox_id: impl Into<String>) -> Self {
self.sandbox_inbox_id = Some(inbox_id.into());
self
}
fn prepare_url(&self) -> String {
if let Some(ref inbox_id) = self.sandbox_inbox_id {
let base = self
.base_url
.as_deref()
.unwrap_or(MAILTRAP_SANDBOX_BASE_URL);
format!("{}{}/{}", base, MAILTRAP_API_ENDPOINT, inbox_id)
} else {
let base = self.base_url.as_deref().unwrap_or(MAILTRAP_BASE_URL);
format!("{}{}", base, MAILTRAP_API_ENDPOINT)
}
}
fn build_request(&self, email: &Email) -> Result<MailtrapRequest, MailError> {
let from = email.from.as_ref().ok_or(MailError::MissingField("from"))?;
if email.to.is_empty() {
return Err(MailError::MissingField("to"));
}
let mut request = MailtrapRequest {
from: MailtrapEmailItem {
email: from.email.clone(),
name: from.name.clone(),
},
to: email
.to
.iter()
.map(|a| MailtrapEmailItem {
email: a.email.clone(),
name: a.name.clone(),
})
.collect(),
cc: if email.cc.is_empty() {
None
} else {
Some(
email
.cc
.iter()
.map(|a| MailtrapEmailItem {
email: a.email.clone(),
name: a.name.clone(),
})
.collect(),
)
},
bcc: if email.bcc.is_empty() {
None
} else {
Some(
email
.bcc
.iter()
.map(|a| MailtrapEmailItem {
email: a.email.clone(),
name: a.name.clone(),
})
.collect(),
)
},
subject: email.subject.clone(),
text: email.text_body.clone(),
html: email.html_body.clone(),
attachments: None,
headers: None,
category: None,
custom_variables: None,
};
if !email.attachments.is_empty() {
request.attachments = Some(
email
.attachments
.iter()
.map(|a| {
let mut attachment = MailtrapAttachment {
filename: a.filename.clone(),
content_type: a.content_type.clone(),
content: a.base64_data(),
disposition: if a.is_inline() {
"inline".to_string()
} else {
"attachment".to_string()
},
content_id: None,
};
if a.is_inline() {
attachment.content_id = Some(a.filename.clone());
}
attachment
})
.collect(),
);
}
let mut headers = email.headers.clone();
if let Some(reply_to) = email.reply_to.first() {
headers.insert("Reply-To".to_string(), reply_to.email.clone());
}
if !headers.is_empty() {
request.headers = Some(headers);
}
if let Some(category) = email.provider_options.get("category") {
request.category = category.as_str().map(|s| s.to_string());
}
if let Some(custom_vars) = email.provider_options.get("custom_variables") {
request.custom_variables = Some(custom_vars.clone());
}
Ok(request)
}
}
#[async_trait]
impl Mailer for MailtrapMailer {
async fn deliver(&self, email: &Email) -> Result<DeliveryResult, MailError> {
let request = self.build_request(email)?;
let url = self.prepare_url();
let response = self
.client
.post(&url)
.header("Content-Type", "application/json")
.header("User-Agent", format!("missive/{}", crate::VERSION))
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&request)
.send()
.await?;
let status = response.status();
if status.is_success() {
let result: MailtrapResponse = response.json().await?;
let message_id = result
.message_ids
.first()
.cloned()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
Ok(DeliveryResult::with_response(
message_id,
serde_json::json!({
"provider": "mailtrap",
"message_ids": result.message_ids,
}),
))
} else {
let error: MailtrapError = response.json().await.unwrap_or(MailtrapError {
errors: vec!["Unknown error".to_string()],
});
Err(MailError::provider_with_status(
"mailtrap",
error.errors.join("; "),
status.as_u16(),
))
}
}
fn provider_name(&self) -> &'static str {
"mailtrap"
}
}
#[derive(Debug, Serialize)]
struct MailtrapRequest {
from: MailtrapEmailItem,
to: Vec<MailtrapEmailItem>,
#[serde(skip_serializing_if = "Option::is_none")]
cc: Option<Vec<MailtrapEmailItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
bcc: Option<Vec<MailtrapEmailItem>>,
subject: String,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
html: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
attachments: Option<Vec<MailtrapAttachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
custom_variables: Option<Value>,
}
#[derive(Debug, Serialize)]
struct MailtrapEmailItem {
email: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
}
#[derive(Debug, Serialize)]
struct MailtrapAttachment {
filename: String,
#[serde(rename = "type")]
content_type: String,
content: String, disposition: String,
#[serde(skip_serializing_if = "Option::is_none")]
content_id: Option<String>,
}
#[derive(Debug, Deserialize)]
struct MailtrapResponse {
#[serde(default)]
message_ids: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct MailtrapError {
#[serde(default)]
errors: Vec<String>,
}