use std::collections::HashMap; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct EmailAddress { pub email: String, pub name: Option, } impl EmailAddress { pub fn new(email: impl Into) -> Self { Self { email: email.into(), name: None, } } pub fn with_name(email: impl Into, name: impl Into) -> 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, } #[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone, PartialEq, Eq)] pub struct SendEmailRequest { pub to: Vec, pub cc: Vec, pub bcc: Vec, pub reply_to: Option, pub subject: String, pub text: Option, pub html: Option, pub attachments: Vec, pub headers: Vec<(String, String)>, pub locale: Option, pub request_id: Option, pub metadata: HashMap, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BatchRecipient { pub to: EmailAddress, pub metadata: HashMap, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct SendBatchEmailRequest { pub recipients: Vec, pub cc: Vec, pub bcc: Vec, pub reply_to: Option, pub subject: String, pub text: Option, pub html: Option, pub attachments: Vec, pub headers: Vec<(String, String)>, pub locale: Option, pub request_id: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct RenderAndSendEmailRequest { pub template_id: String, pub locale: Option, pub to: EmailAddress, pub cc: Vec, pub bcc: Vec, pub reply_to: Option, pub subject: String, pub vars: serde_json::Value, pub attachments: Vec, pub request_id: Option, pub metadata: HashMap, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct EmailSent { pub request_id: Option, pub message_id: String, pub recipient_count: usize, pub metadata: HashMap, pub sent_at_ms: i64, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct EmailFailed { pub request_id: Option, pub error: String, pub recipient_count: usize, pub metadata: HashMap, 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("

plain

".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: "".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); } }