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 RESEND_API_URL: &str = "https://api.resend.com";
pub struct ResendMailer {
api_key: String,
client: Client,
base_url: String,
}
impl ResendMailer {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
client: Client::new(),
base_url: RESEND_API_URL.to_string(),
}
}
pub fn with_client(api_key: impl Into<String>, client: Client) -> Self {
Self {
api_key: api_key.into(),
client,
base_url: RESEND_API_URL.to_string(),
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
fn build_request(&self, email: &Email) -> Result<ResendRequest, 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 = ResendRequest {
from: from.formatted(),
to: email.to.iter().map(|a| a.formatted()).collect(),
subject: if email.subject.is_empty() {
None
} else {
Some(email.subject.clone())
},
html: email.html_body.clone(),
text: email.text_body.clone(),
cc: if email.cc.is_empty() {
None
} else {
Some(email.cc.iter().map(|a| a.formatted()).collect())
},
bcc: if email.bcc.is_empty() {
None
} else {
Some(email.bcc.iter().map(|a| a.formatted()).collect())
},
reply_to: email.reply_to.first().map(|a| a.formatted()),
headers: if email.headers.is_empty() {
None
} else {
Some(
email
.headers
.iter()
.map(|(k, v)| ResendHeader {
name: k.clone(),
value: v.clone(),
})
.collect(),
)
},
attachments: None,
tags: None,
scheduled_at: None,
template: None,
};
if !email.attachments.is_empty() {
let attachments: Vec<ResendAttachment> = email
.attachments
.iter()
.map(|a| {
let content_id = if a.is_inline() {
a.content_id.clone()
} else {
None
};
ResendAttachment {
filename: a.filename.clone(),
content: a.base64_data(),
content_type: Some(a.content_type.clone()),
content_id,
}
})
.collect();
request.attachments = Some(attachments);
}
if let Some(tags) = email.provider_options.get("tags") {
request.tags = serde_json::from_value(tags.clone()).ok();
}
if let Some(scheduled_at) = email.provider_options.get("scheduled_at") {
request.scheduled_at = scheduled_at.as_str().map(|s| s.to_string());
}
if let Some(template) = email.provider_options.get("template") {
request.template = Some(template.clone());
}
Ok(request)
}
}
#[async_trait]
impl Mailer for ResendMailer {
async fn deliver(&self, email: &Email) -> Result<DeliveryResult, MailError> {
let request = self.build_request(email)?;
let url = format!("{}/emails", self.base_url);
let mut req = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.header("User-Agent", format!("missive/{}", crate::VERSION));
if let Some(idempotency_key) = email.provider_options.get("idempotency_key") {
if let Some(key) = idempotency_key.as_str() {
req = req.header("Idempotency-Key", key);
}
}
let response = req.json(&request).send().await?;
let status = response.status();
if status.is_success() {
let result: ResendResponse = response.json().await?;
Ok(DeliveryResult::with_response(
result.id,
serde_json::json!({ "provider": "resend" }),
))
} else {
let error: ResendError = response.json().await.unwrap_or(ResendError {
message: "Unknown error".to_string(),
name: None,
});
Err(MailError::provider_with_status(
"resend",
error.message,
status.as_u16(),
))
}
}
fn validate_batch(&self, emails: &[Email]) -> Result<(), MailError> {
for (i, email) in emails.iter().enumerate() {
if email.provider_options.contains_key("scheduled_at") {
return Err(MailError::UnsupportedFeature(format!(
"scheduled_at is not supported in batch sends (email {})",
i + 1
)));
}
if !email.attachments.is_empty() {
return Err(MailError::UnsupportedFeature(format!(
"attachments are not supported in Resend batch sends (email {})",
i + 1
)));
}
}
Ok(())
}
async fn deliver_many(&self, emails: &[Email]) -> Result<Vec<DeliveryResult>, MailError> {
if emails.is_empty() {
return Ok(vec![]);
}
self.validate_batch(emails)?;
let requests: Vec<ResendRequest> = emails
.iter()
.map(|email| self.build_request(email))
.collect::<Result<Vec<_>, _>>()?;
let url = format!("{}/emails/batch", self.base_url);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.header("User-Agent", format!("missive/{}", crate::VERSION))
.json(&requests)
.send()
.await?;
let status = response.status();
if status.is_success() {
let result: ResendBatchResponse = response.json().await?;
Ok(result
.data
.into_iter()
.map(|r| {
DeliveryResult::with_response(r.id, serde_json::json!({ "provider": "resend" }))
})
.collect())
} else {
let error: ResendError = response.json().await.unwrap_or(ResendError {
message: "Unknown error".to_string(),
name: None,
});
Err(MailError::provider_with_status(
"resend",
error.message,
status.as_u16(),
))
}
}
fn provider_name(&self) -> &'static str {
"resend"
}
}
#[derive(Debug, Serialize)]
struct ResendRequest {
from: String,
to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
html: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cc: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
bcc: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
reply_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<Vec<ResendHeader>>,
#[serde(skip_serializing_if = "Option::is_none")]
attachments: Option<Vec<ResendAttachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
tags: Option<Vec<ResendTag>>,
#[serde(skip_serializing_if = "Option::is_none")]
scheduled_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
template: Option<Value>,
}
#[derive(Debug, Serialize)]
struct ResendHeader {
name: String,
value: String,
}
#[derive(Debug, Serialize)]
struct ResendAttachment {
filename: String,
content: String, #[serde(skip_serializing_if = "Option::is_none")]
content_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
content_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ResendTag {
name: String,
value: String,
}
#[derive(Debug, Deserialize)]
struct ResendResponse {
id: String,
}
#[derive(Debug, Deserialize)]
struct ResendBatchResponse {
data: Vec<ResendResponse>,
}
#[derive(Debug, Deserialize)]
struct ResendError {
message: String,
#[serde(default)]
#[allow(dead_code)]
name: Option<String>,
}