use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAddress {
pub email: String,
pub name: Option<String>,
}
impl EmailAddress {
pub fn new(email: impl Into<String>) -> Self {
let email_str = email.into();
Self::parse(&email_str)
}
fn parse(email_str: &str) -> Self {
let trimmed = email_str.trim();
if let Some(start) = trimmed.find('<') {
if let Some(end) = trimmed.find('>') {
if end > start {
let email_part = trimmed[start + 1..end].trim();
let name_part = trimmed[..start].trim().trim_matches('"').to_string();
return Self {
email: email_part.to_string(),
name: if name_part.is_empty() { None } else { Some(name_part) },
};
}
}
}
Self {
email: trimmed.to_string(),
name: None,
}
}
pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
Self {
email: email.into(),
name: Some(name.into()),
}
}
pub fn is_valid(&self) -> bool {
self.email.contains('@') && self.email.contains('.')
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
match &self.name {
Some(name) => format!("{} <{}>", name, self.email),
None => self.email.clone(),
}
}
}
impl From<&str> for EmailAddress {
fn from(email: &str) -> Self {
Self::new(email)
}
}
impl From<String> for EmailAddress {
fn from(email: String) -> Self {
Self::new(email)
}
}
impl From<&String> for EmailAddress {
fn from(email: &String) -> Self {
Self::new(email.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAttachment {
pub filename: String,
pub content_type: String,
pub content: Vec<u8>,
pub content_id: Option<String>,
pub inline: bool,
}
impl EmailAttachment {
pub fn new(
filename: impl Into<String>,
content_type: impl Into<String>,
content: Vec<u8>,
) -> Self {
Self {
filename: filename.into(),
content_type: content_type.into(),
content,
content_id: None,
inline: false,
}
}
pub fn inline(
filename: impl Into<String>,
content_type: impl Into<String>,
content: Vec<u8>,
content_id: impl Into<String>,
) -> Self {
Self {
filename: filename.into(),
content_type: content_type.into(),
content,
content_id: Some(content_id.into()),
inline: true,
}
}
pub fn size(&self) -> usize {
self.content.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailMessage {
pub id: String,
pub from: EmailAddress,
pub reply_to: Option<EmailAddress>,
pub recipients: EmailRecipients,
pub subject: String,
pub text: Option<String>,
pub html: Option<String>,
pub attachments: Vec<EmailAttachment>,
pub headers: HashMap<String, String>,
pub template_data: Option<HashMap<String, serde_json::Value>>,
pub created_at: DateTime<Utc>,
pub scheduled_at: Option<DateTime<Utc>>,
pub priority: EmailPriority,
pub tracking: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailRecipients {
pub to: Vec<EmailAddress>,
pub cc: Vec<EmailAddress>,
pub bcc: Vec<EmailAddress>,
}
impl EmailRecipients {
pub fn new(to: Vec<EmailAddress>) -> Self {
Self {
to,
cc: Vec::new(),
bcc: Vec::new(),
}
}
pub fn with_cc_bcc(
to: Vec<EmailAddress>,
cc: Vec<EmailAddress>,
bcc: Vec<EmailAddress>,
) -> Self {
Self { to, cc, bcc }
}
pub fn add_to(&mut self, recipient: EmailAddress) {
self.to.push(recipient);
}
pub fn add_cc(&mut self, recipient: EmailAddress) {
self.cc.push(recipient);
}
pub fn add_bcc(&mut self, recipient: EmailAddress) {
self.bcc.push(recipient);
}
pub fn all(&self) -> Vec<&EmailAddress> {
self.to.iter()
.chain(self.cc.iter())
.chain(self.bcc.iter())
.collect()
}
pub fn count(&self) -> usize {
self.to.len() + self.cc.len() + self.bcc.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum EmailPriority {
Low = 1,
#[default]
Normal = 3,
High = 5,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EmailStatus {
Pending,
Sent,
Failed,
Bounced,
Opened,
Clicked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailDeliveryReport {
pub message_id: String,
pub status: EmailStatus,
pub provider_message_id: Option<String>,
pub delivered_at: Option<DateTime<Utc>>,
pub error: Option<String>,
pub bounce_reason: Option<String>,
pub opened_at: Option<DateTime<Utc>>,
pub clicked_at: Vec<DateTime<Utc>>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailTemplate {
pub name: String,
pub subject: String,
pub html_template: String,
pub text_template: Option<String>,
pub description: Option<String>,
pub variables: Option<HashMap<String, String>>,
}
impl EmailMessage {
pub fn builder() -> EmailMessageBuilder {
EmailMessageBuilder::new()
}
pub fn validate(&self) -> Result<(), String> {
if !self.from.is_valid() {
return Err("Invalid sender email address".to_string());
}
if self.recipients.to.is_empty() {
return Err("At least one 'to' recipient is required".to_string());
}
for recipient in self.recipients.all() {
if !recipient.is_valid() {
return Err(format!("Invalid recipient email address: {}", recipient.email));
}
}
if self.subject.is_empty() {
return Err("Email subject cannot be empty".to_string());
}
if self.text.is_none() && self.html.is_none() {
return Err("Email must have either text or HTML content".to_string());
}
Ok(())
}
pub fn primary_content(&self) -> Option<&str> {
self.html.as_deref().or(self.text.as_deref())
}
pub fn has_attachments(&self) -> bool {
!self.attachments.is_empty()
}
pub fn attachment_size(&self) -> usize {
self.attachments.iter().map(|a| a.size()).sum()
}
}
pub struct EmailMessageBuilder {
message: EmailMessage,
}
impl EmailMessageBuilder {
pub fn new() -> Self {
Self {
message: EmailMessage {
id: Uuid::new_v4().to_string(),
from: EmailAddress::new(""),
reply_to: None,
recipients: EmailRecipients::new(Vec::new()),
subject: String::new(),
text: None,
html: None,
attachments: Vec::new(),
headers: HashMap::new(),
template_data: None,
created_at: Utc::now(),
scheduled_at: None,
priority: EmailPriority::Normal,
tracking: false,
},
}
}
pub fn from(mut self, from: impl Into<EmailAddress>) -> Self {
self.message.from = from.into();
self
}
pub fn reply_to(mut self, reply_to: impl Into<EmailAddress>) -> Self {
self.message.reply_to = Some(reply_to.into());
self
}
pub fn to(mut self, to: impl Into<EmailAddress>) -> Self {
self.message.recipients.to.push(to.into());
self
}
pub fn to_many(mut self, to: Vec<EmailAddress>) -> Self {
self.message.recipients.to.extend(to);
self
}
pub fn cc(mut self, cc: impl Into<EmailAddress>) -> Self {
self.message.recipients.cc.push(cc.into());
self
}
pub fn bcc(mut self, bcc: impl Into<EmailAddress>) -> Self {
self.message.recipients.bcc.push(bcc.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.message.subject = subject.into();
self
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.message.text = Some(text.into());
self
}
pub fn html(mut self, html: impl Into<String>) -> Self {
self.message.html = Some(html.into());
self
}
pub fn attachment(mut self, attachment: EmailAttachment) -> Self {
self.message.attachments.push(attachment);
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.message.headers.insert(key.into(), value.into());
self
}
pub fn template_data(mut self, data: HashMap<String, serde_json::Value>) -> Self {
self.message.template_data = Some(data);
self
}
pub fn scheduled_at(mut self, time: DateTime<Utc>) -> Self {
self.message.scheduled_at = Some(time);
self
}
pub fn priority(mut self, priority: EmailPriority) -> Self {
self.message.priority = priority;
self
}
pub fn tracking(mut self, enabled: bool) -> Self {
self.message.tracking = enabled;
self
}
pub fn build(self) -> EmailMessage {
self.message
}
}
impl Default for EmailMessageBuilder {
fn default() -> Self {
Self::new()
}
}