a06673b
use std::collections::HashMap;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct EmailAddress {
pub email: String,
pub name: Option<String>,
}
impl EmailAddress {
pub fn new(email: impl Into<String>) -> Self {
Self {
email: email.into(),
name: None,
}
}
pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
Self {
email: email.into(),
name: Some(name.into()),
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Attachment {
pub filename: String,
pub content_type: String,
pub data_b64: String,
pub content_id: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone, PartialEq, Eq)]
pub struct SendEmailRequest {
pub to: Vec<EmailAddress>,
pub cc: Vec<EmailAddress>,
pub bcc: Vec<EmailAddress>,
pub reply_to: Option<EmailAddress>,
pub subject: String,
pub text: Option<String>,
pub html: Option<String>,
pub attachments: Vec<Attachment>,
pub headers: Vec<(String, String)>,
pub locale: Option<String>,
pub request_id: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct BatchRecipient {
pub to: EmailAddress,
pub metadata: HashMap<String, String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SendBatchEmailRequest {
pub recipients: Vec<BatchRecipient>,
pub cc: Vec<EmailAddress>,
pub bcc: Vec<EmailAddress>,
pub reply_to: Option<EmailAddress>,
pub subject: String,
pub text: Option<String>,
pub html: Option<String>,
pub attachments: Vec<Attachment>,
pub headers: Vec<(String, String)>,
pub locale: Option<String>,
pub request_id: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct RenderAndSendEmailRequest {
pub template_id: String,
pub locale: Option<String>,
pub to: EmailAddress,
pub cc: Vec<EmailAddress>,
pub bcc: Vec<EmailAddress>,
pub reply_to: Option<EmailAddress>,
pub subject: String,
pub vars: serde_json::Value,
pub attachments: Vec<Attachment>,
pub request_id: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct EmailSent {
pub request_id: Option<String>,
pub message_id: String,
pub recipient_count: usize,
pub metadata: HashMap<String, String>,
pub sent_at_ms: i64,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct EmailFailed {
pub request_id: Option<String>,
pub error: String,
pub recipient_count: usize,
pub metadata: HashMap<String, String>,
pub failed_at_ms: i64,
}
pub fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn send_email_request_serde_roundtrip() {
let mut meta = HashMap::new();
meta.insert("tenant".to_string(), "acme".to_string());
let req = SendEmailRequest {
to: vec![EmailAddress::with_name("a@example.com", "Alice")],
subject: "hi".into(),
text: Some("plain".into()),
html: Some("<p>plain</p>".into()),
metadata: meta,
..Default::default()
};
let bytes = serde_json::to_vec(&req).unwrap();
let back: SendEmailRequest = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, req);
}
#[test]
fn email_sent_serde_roundtrip() {
let mut meta = HashMap::new();
meta.insert("k".into(), "v".into());
let sent = EmailSent {
request_id: Some("req-1".into()),
message_id: "<abc@example.com>".into(),
recipient_count: 3,
metadata: meta,
sent_at_ms: 1_700_000_000_000,
};
let bytes = serde_json::to_vec(&sent).unwrap();
let back: EmailSent = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, sent);
}
#[test]
fn email_failed_serde_roundtrip() {
let failed = EmailFailed {
request_id: Some("req-1".into()),
error: "smtp timeout".into(),
recipient_count: 1,
metadata: HashMap::new(),
failed_at_ms: 42,
};
let bytes = serde_json::to_vec(&failed).unwrap();
let back: EmailFailed = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, failed);
}
#[test]
fn render_and_send_serde_roundtrip() {
let req = RenderAndSendEmailRequest {
template_id: "welcome".into(),
locale: Some("zh".into()),
to: EmailAddress::new("a@example.com"),
cc: vec![],
bcc: vec![],
reply_to: None,
subject: "Welcome {{name}}".into(),
vars: serde_json::json!({"name":"Alice"}),
attachments: vec![],
request_id: Some("r1".into()),
metadata: HashMap::new(),
};
let bytes = serde_json::to_vec(&req).unwrap();
let back: RenderAndSendEmailRequest = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, req);
}
#[test]
fn send_batch_serde_roundtrip() {
let req = SendBatchEmailRequest {
recipients: vec![BatchRecipient {
to: EmailAddress::new("a@example.com"),
metadata: HashMap::new(),
}],
cc: vec![],
bcc: vec![],
reply_to: None,
subject: "newsletter".into(),
text: Some("hi".into()),
html: None,
attachments: vec![],
headers: vec![],
locale: None,
request_id: Some("r".into()),
};
let bytes = serde_json::to_vec(&req).unwrap();
let back: SendBatchEmailRequest = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, req);
}
#[test]
fn now_ms_is_positive() {
assert!(now_ms() > 1_700_000_000_000);
}
}