use anyhow::{Error, Result, anyhow};
use reqwest::{Client, Method, Url};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{self, Display};
const DEFAULT_URL: &str = "https://send.api.mailtrap.io/";
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Address {
pub name: String,
pub email: String,
}
impl Address {
pub fn new() -> Self {
Self {
name: String::new(),
email: String::new(),
}
}
pub fn to_string(&self) -> String {
if self.name.is_empty() {
return self.email.clone();
}
format!("\"{}\" <{}>", self.name, self.email)
}
pub fn from_string(address: String) -> Result<Self, Error> {
if address.is_empty() {
return Err(anyhow!("Empty address"));
}
if address.contains('<') {
let parts = address.splitn(2, '<').collect::<Vec<&str>>();
if parts.len() == 1 {
return Ok(Self::new().email(&address));
}
let name = parts[0].trim().replace('"', "").to_string();
let email = parts[1].trim().replace('>', "").to_string();
return Ok(Self::new().name(&name).email(&email));
}
Ok(Self::new().email(&address))
}
pub fn from_str(address: &str) -> Result<Self, Error> {
Self::from_string(address.to_string())
}
pub fn name(mut self, name: &str) -> Self {
self.name = name.to_string();
self
}
pub fn email(mut self, email: &str) -> Self {
self.email = email.to_string();
self
}
}
impl Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum ContentType {
Plain,
Html,
}
impl ContentType {
pub fn new(content_type: &str) -> Result<Self, Error> {
match content_type {
"text/plain" => Ok(Self::Plain),
"text/html" => Ok(Self::Html),
_ => Err(anyhow!("Invalid content type: {}", content_type)),
}
}
pub fn to_string(&self) -> String {
match self {
Self::Plain => "text/plain".to_string(),
Self::Html => "text/html".to_string(),
}
}
pub fn from_string(content_type: String) -> Result<Self, Error> {
Self::new(&content_type.as_str())
}
pub fn from_str(content_type: &str) -> Result<Self, Error> {
Self::new(content_type)
}
}
impl Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum Disposition {
Attachment,
Inline,
}
impl Disposition {
pub fn new(disposition: &str) -> Result<Self, Error> {
match disposition {
"attachment" => Ok(Self::Attachment),
"inline" => Ok(Self::Inline),
_ => Err(anyhow!("Invalid disposition: {}", disposition)),
}
}
pub fn to_string(&self) -> String {
match self {
Self::Attachment => "attachment".to_string(),
Self::Inline => "inline".to_string(),
}
}
pub fn from_string(disposition: String) -> Result<Self, Error> {
Self::new(&disposition.as_str())
}
pub fn from_str(disposition: &str) -> Result<Self, Error> {
Self::new(disposition)
}
}
impl Display for Disposition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Attachment {
pub content: Vec<u8>,
pub content_type: ContentType,
pub filename: String,
pub disposition: Disposition,
pub content_id: Option<Vec<u8>>,
}
impl Attachment {
pub fn new() -> Self {
Self {
content: Vec::new(),
content_type: ContentType::new("text/plain").unwrap(),
filename: String::new(),
disposition: Disposition::new("attachment").unwrap(),
content_id: None,
}
}
pub fn content(mut self, content: Vec<u8>) -> Self {
self.content = content;
self
}
pub fn content_type(mut self, content_type: ContentType) -> Self {
self.content_type = content_type;
self
}
pub fn filename(mut self, filename: &str) -> Self {
self.filename = filename.to_string();
self
}
pub fn disposition(mut self, disposition: Disposition) -> Self {
self.disposition = disposition;
self
}
pub fn content_id(mut self, content_id: Option<Vec<u8>>) -> Self {
self.content_id = content_id;
self
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Header {
pub key: String,
pub value: String,
}
impl Header {
pub fn new() -> Self {
Self {
key: String::new(),
value: String::new(),
}
}
pub fn from_string(header: String) -> Result<Self, Error> {
if header.is_empty() {
return Err(anyhow!("Empty header"));
}
if !header.contains(':') {
return Err(anyhow!("Invalid header: {}", header));
}
let parts = header.splitn(2, ':').collect::<Vec<&str>>();
if parts.len() == 1 {
return Err(anyhow!("Invalid header: {}", header));
}
let key = parts[0].trim().to_string();
let value = parts[1].trim().to_string();
Ok(Self::new().key(&key).value(&value))
}
pub fn from_str(header: &str) -> Result<Self, Error> {
Self::from_string(header.to_string())
}
pub fn key(mut self, key: &str) -> Self {
self.key = key.to_string();
self
}
pub fn value(mut self, value: &str) -> Self {
self.value = value.to_string();
self
}
}
impl Display for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.key, self.value)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Email {
pub from: Address,
pub to: Address,
pub cc: Vec<Address>,
pub bcc: Vec<Address>,
pub reply_to: Address,
pub attachments: Vec<Attachment>,
pub headers: HashMap<String, Header>,
pub subject: String,
pub html: String,
pub text: String,
pub category: String,
}
impl Email {
pub fn new() -> Self {
Self {
from: Address::new(),
to: Address::new(),
cc: Vec::new(),
bcc: Vec::new(),
reply_to: Address::new(),
attachments: Vec::new(),
headers: HashMap::new(),
subject: String::new(),
html: String::new(),
text: String::new(),
category: String::new(),
}
}
pub fn from(mut self, from: &str) -> Self {
self.from = Address::from_str(from).unwrap();
self
}
pub fn to(mut self, to: &str) -> Self {
self.to = Address::from_str(to).unwrap();
self
}
pub fn ccs(mut self, ccs: Vec<&str>) -> Self {
for cc in ccs {
self.cc.push(Address::from_str(cc).unwrap());
}
self
}
pub fn cc(mut self, cc: &str) -> Self {
self.cc.push(Address::from_str(cc).unwrap());
self
}
pub fn bccs(mut self, bccs: Vec<&str>) -> Self {
for bcc in bccs {
self.bcc.push(Address::from_str(bcc).unwrap());
}
self
}
pub fn bcc(mut self, bcc: &str) -> Self {
self.bcc.push(Address::from_str(bcc).unwrap());
self
}
pub fn reply_to(mut self, reply_to: &str) -> Self {
self.reply_to = Address::from_str(reply_to).unwrap();
self
}
pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
self.attachments = attachments;
self
}
pub fn headers(mut self, headers: Vec<Header>) -> Self {
for header in headers {
self.headers.insert(header.key.clone(), header);
}
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers
.insert(key.to_string(), Header::new().key(key).value(value));
self
}
pub fn subject(mut self, subject: &str) -> Self {
self.subject = subject.to_string();
self
}
pub fn html(mut self, html: &str) -> Self {
self.html = html.to_string();
self
}
pub fn text(mut self, text: &str) -> Self {
self.text = text.to_string();
self
}
pub fn category(mut self, category: &str) -> Self {
self.category = category.to_string();
self
}
pub async fn send(
&self,
url: Option<&str>,
api_key: Option<&str>,
bearer_token: Option<&str>,
) -> Result<bool, Error> {
let url = match url {
Some(url) => {
if url.is_empty() {
return Err(anyhow!("URL is empty"));
}
let url = match Url::parse(url) {
Ok(url) => url,
Err(e) => return Err(anyhow!("Invalid URL: {}", e)),
};
url
}
None => {
let url = match Url::parse(DEFAULT_URL) {
Ok(url) => url,
Err(e) => return Err(anyhow!("Invalid URL: {}", e)),
};
url
}
};
if api_key.is_none() && bearer_token.is_none() {
return Err(anyhow!("API key or bearer token is required"));
}
if api_key.is_some_and(|key| key.is_empty()) {
return Err(anyhow!("API key is empty"));
}
if bearer_token.is_some_and(|token| token.is_empty()) {
return Err(anyhow!("Bearer token is empty"));
}
if self.to.email.is_empty() {
return Err(anyhow!("To email is empty"));
}
if self.from.email.is_empty() {
return Err(anyhow!("From email is empty"));
}
if self.subject.is_empty() {
return Err(anyhow!("Subject is empty"));
}
if self.text.is_empty() && self.html.is_empty() {
return Err(anyhow!("Text or HTML is empty"));
}
let client = Client::new();
let mut request = client.request(Method::POST, format!("{}api/send", url));
if api_key.is_some() {
request = request.header("Api-Token", api_key.unwrap());
}
if bearer_token.is_some() {
request = request.header("Authorization", format!("Bearer {}", bearer_token.unwrap()));
}
let response = match request.json(&self).send().await {
Ok(response) => response,
Err(e) => return Err(anyhow!("Failed to send email: {}", e)),
};
let status = response.status();
let body = match response.json::<EmailResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
if !status.is_success() {
return Err(anyhow!("Failed to send email: {}", body.errors.join(": ")));
}
Ok(body.success)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EmailResponse {
pub success: bool,
pub errors: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct BatchEmailRequest {
pub from: Address,
pub to: Address,
pub cc: Vec<Address>,
pub bcc: Vec<Address>,
pub reply_to: Address,
pub subject: String,
pub html: String,
pub text: String,
pub attachments: Vec<Attachment>,
pub headers: HashMap<String, Header>,
pub category: String,
pub custom_variables: HashMap<String, String>,
pub template_uuid: String,
pub template_variables: HashMap<String, String>,
}
impl BatchEmailRequest {
pub fn new() -> Self {
Self {
from: Address::new(),
to: Address::new(),
cc: Vec::new(),
bcc: Vec::new(),
reply_to: Address::new(),
subject: String::new(),
html: String::new(),
text: String::new(),
attachments: Vec::new(),
headers: HashMap::new(),
category: String::new(),
custom_variables: HashMap::new(),
template_uuid: String::new(),
template_variables: HashMap::new(),
}
}
pub fn from(mut self, from: &str) -> Self {
self.from = Address::from_str(from).unwrap();
self
}
pub fn to(mut self, to: &str) -> Self {
self.to = Address::from_str(to).unwrap();
self
}
pub fn ccs(mut self, ccs: Vec<&str>) -> Self {
for cc in ccs {
self.cc.push(Address::from_str(cc).unwrap());
}
self
}
pub fn cc(mut self, cc: &str) -> Self {
self.cc.push(Address::from_str(cc).unwrap());
self
}
pub fn bccs(mut self, bccs: Vec<&str>) -> Self {
for bcc in bccs {
self.bcc.push(Address::from_str(bcc).unwrap());
}
self
}
pub fn bcc(mut self, bcc: &str) -> Self {
self.bcc.push(Address::from_str(bcc).unwrap());
self
}
pub fn reply_to(mut self, reply_to: &str) -> Self {
self.reply_to = Address::from_str(reply_to).unwrap();
self
}
pub fn subject(mut self, subject: &str) -> Self {
self.subject = subject.to_string();
self
}
pub fn html(mut self, html: &str) -> Self {
self.html = html.to_string();
self
}
pub fn text(mut self, text: &str) -> Self {
self.text = text.to_string();
self
}
pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
self.attachments = attachments;
self
}
pub fn attachment(mut self, attachment: Attachment) -> Self {
self.attachments.push(attachment);
self
}
pub fn headers(mut self, headers: Vec<Header>) -> Self {
for header in headers {
self.headers.insert(header.key.clone(), header);
}
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers
.insert(key.to_string(), Header::new().key(key).value(value));
self
}
pub fn category(mut self, category: &str) -> Self {
self.category = category.to_string();
self
}
pub fn custom_variables(mut self, custom_variables: Vec<(&str, &str)>) -> Self {
for (key, value) in custom_variables {
self.custom_variables
.insert(key.to_string(), value.to_string().to_string());
}
self
}
pub fn custom_variable(mut self, key: &str, value: &str) -> Self {
self.custom_variables
.insert(key.to_string(), value.to_string());
self
}
pub fn template_uuid(mut self, template_uuid: &str) -> Self {
self.template_uuid = template_uuid.to_string();
self
}
pub fn template_variables(mut self, template_variables: Vec<(&str, &str)>) -> Self {
for (key, value) in template_variables {
self.template_variables
.insert(key.to_string(), value.to_string().to_string());
}
self
}
pub fn template_variable(mut self, key: &str, value: &str) -> Self {
self.template_variables
.insert(key.to_string(), value.to_string());
self
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct BatchEmail {
pub from: Address,
pub reply_to: Address,
pub subject: String,
pub html: String,
pub text: String,
pub attachments: Vec<Attachment>,
pub headers: HashMap<String, Header>,
pub category: String,
pub custom_variables: HashMap<String, String>,
pub template_uuid: String,
pub template_variables: HashMap<String, String>,
pub requests: Vec<BatchEmailRequest>,
}
impl BatchEmail {
pub fn new() -> Self {
Self {
from: Address::new(),
reply_to: Address::new(),
subject: String::new(),
html: String::new(),
text: String::new(),
attachments: Vec::new(),
headers: HashMap::new(),
category: String::new(),
custom_variables: HashMap::new(),
template_uuid: String::new(),
template_variables: HashMap::new(),
requests: Vec::new(),
}
}
pub fn from(mut self, from: &str) -> Self {
self.from = Address::from_str(from).unwrap();
self
}
pub fn reply_to(mut self, reply_to: &str) -> Self {
self.reply_to = Address::from_str(reply_to).unwrap();
self
}
pub fn subject(mut self, subject: &str) -> Self {
self.subject = subject.to_string();
self
}
pub fn html(mut self, html: &str) -> Self {
self.html = html.to_string();
self
}
pub fn text(mut self, text: &str) -> Self {
self.text = text.to_string();
self
}
pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
self.attachments = attachments;
self
}
pub fn attachment(mut self, attachment: Attachment) -> Self {
self.attachments.push(attachment);
self
}
pub fn headers(mut self, headers: Vec<Header>) -> Self {
for header in headers {
self.headers.insert(header.key.clone(), header);
}
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers
.insert(key.to_string(), Header::new().key(key).value(value));
self
}
pub fn category(mut self, category: &str) -> Self {
self.category = category.to_string();
self
}
pub fn custom_variables(mut self, custom_variables: Vec<(&str, &str)>) -> Self {
for (key, value) in custom_variables {
self.custom_variables
.insert(key.to_string(), value.to_string().to_string());
}
self
}
pub fn custom_variable(mut self, key: &str, value: &str) -> Self {
self.custom_variables
.insert(key.to_string(), value.to_string());
self
}
pub fn template_uuid(mut self, template_uuid: &str) -> Self {
self.template_uuid = template_uuid.to_string();
self
}
pub fn template_variables(mut self, template_variables: Vec<(&str, &str)>) -> Self {
for (key, value) in template_variables {
self.template_variables
.insert(key.to_string(), value.to_string().to_string());
}
self
}
pub fn template_variable(mut self, key: &str, value: &str) -> Self {
self.template_variables
.insert(key.to_string(), value.to_string());
self
}
pub fn requests(mut self, requests: Vec<BatchEmailRequest>) -> Self {
self.requests = requests;
self
}
pub fn request(mut self, request: BatchEmailRequest) -> Self {
self.requests.push(request);
self
}
pub async fn send(
&self,
url: Option<&str>,
api_key: Option<&str>,
bearer_token: Option<&str>,
) -> Result<bool, Error> {
let url = match url {
Some(url) => {
if url.is_empty() {
return Err(anyhow!("URL is empty"));
}
let url = match Url::parse(url) {
Ok(url) => url,
Err(e) => return Err(anyhow!("Invalid URL: {}", e)),
};
url
}
None => {
let url = match Url::parse(DEFAULT_URL) {
Ok(url) => url,
Err(e) => return Err(anyhow!("Invalid URL: {}", e)),
};
url
}
};
if api_key.is_none() && bearer_token.is_none() {
return Err(anyhow!("API key or bearer token is required"));
}
if api_key.is_some_and(|key| key.is_empty()) {
return Err(anyhow!("API key is empty"));
}
if bearer_token.is_some_and(|token| token.is_empty()) {
return Err(anyhow!("Bearer token is empty"));
}
if self.from.email.is_empty() {
return Err(anyhow!("From email is empty"));
}
if self.reply_to.email.is_empty() {
return Err(anyhow!("Reply-To email is empty"));
}
if self.subject.is_empty() {
return Err(anyhow!("Subject is empty"));
}
if self.text.is_empty() && self.html.is_empty() {
return Err(anyhow!("Text or HTML is empty"));
}
if self.category.is_empty() {
return Err(anyhow!("Category is empty"));
}
if self.template_uuid.is_empty() {
return Err(anyhow!("Template UUID is empty"));
}
if self.requests.is_empty() {
return Err(anyhow!("Requests are empty"));
}
for request in &self.requests {
if request.from.email.is_empty() {
return Err(anyhow!("Request from email is empty"));
}
if request.to.email.is_empty() {
return Err(anyhow!("Request to email is empty"));
}
if request.reply_to.email.is_empty() {
return Err(anyhow!("Request reply-to email is empty"));
}
if request.subject.is_empty() {
return Err(anyhow!("Request subject is empty"));
}
if request.text.is_empty() && request.html.is_empty() {
return Err(anyhow!("Request text or HTML is empty"));
}
if request.category.is_empty() {
return Err(anyhow!("Request category is empty"));
}
if request.template_uuid.is_empty() {
return Err(anyhow!("Request template UUID is empty"));
}
}
let client = Client::new();
let mut request = client.request(Method::POST, format!("{}api/batch", url));
if api_key.is_some() {
request = request.header("Api-Token", api_key.unwrap());
}
if bearer_token.is_some() {
request = request.header("Authorization", format!("Bearer {}", bearer_token.unwrap()));
}
let response = match request.json(&self).send().await {
Ok(response) => response,
Err(e) => return Err(anyhow!("Failed to send batch email: {}", e)),
};
let status = response.status();
let body = match response.json::<BatchEmailResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
if !status.is_success() {
return Err(anyhow!(
"Failed to send batch email: {}",
body.errors.join(": ")
));
}
Ok(body.success)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct BatchEmailInnerResponse {
pub success: bool,
pub message_ids: Vec<String>,
pub errors: Vec<String>,
}
impl BatchEmailInnerResponse {
pub fn new() -> Self {
Self {
success: false,
message_ids: Vec::new(),
errors: Vec::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct BatchEmailResponse {
pub success: bool,
pub responses: Vec<BatchEmailInnerResponse>,
pub errors: Vec<String>,
}
impl BatchEmailResponse {
pub fn new() -> Self {
Self {
success: false,
responses: Vec::new(),
errors: Vec::new(),
}
}
}