1use serde::{Serialize, Deserialize};
4use std::collections::HashMap;
5use chrono::{DateTime, Utc};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct EmailAddress {
11 pub email: String,
13
14 pub name: Option<String>,
16}
17
18impl EmailAddress {
19 pub fn new(email: impl Into<String>) -> Self {
22 let email_str = email.into();
23 Self::parse(&email_str)
24 }
25
26 fn parse(email_str: &str) -> Self {
28 let trimmed = email_str.trim();
29
30 if let Some(start) = trimmed.find('<') {
32 if let Some(end) = trimmed.find('>') {
33 if end > start {
34 let email_part = trimmed[start + 1..end].trim();
35 let name_part = trimmed[..start].trim().trim_matches('"').to_string();
36
37 return Self {
38 email: email_part.to_string(),
39 name: if name_part.is_empty() { None } else { Some(name_part) },
40 };
41 }
42 }
43 }
44
45 Self {
47 email: trimmed.to_string(),
48 name: None,
49 }
50 }
51
52 pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
54 Self {
55 email: email.into(),
56 name: Some(name.into()),
57 }
58 }
59
60 pub fn is_valid(&self) -> bool {
62 self.email.contains('@') && self.email.contains('.')
63 }
64
65 #[allow(clippy::inherent_to_string)]
67 pub fn to_string(&self) -> String {
68 match &self.name {
69 Some(name) => format!("{} <{}>", name, self.email),
70 None => self.email.clone(),
71 }
72 }
73}
74
75impl From<&str> for EmailAddress {
76 fn from(email: &str) -> Self {
77 Self::new(email)
78 }
79}
80
81impl From<String> for EmailAddress {
82 fn from(email: String) -> Self {
83 Self::new(email)
84 }
85}
86
87impl From<&String> for EmailAddress {
88 fn from(email: &String) -> Self {
89 Self::new(email.as_str())
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EmailAttachment {
96 pub filename: String,
98
99 pub content_type: String,
101
102 pub content: Vec<u8>,
104
105 pub content_id: Option<String>,
107
108 pub inline: bool,
110}
111
112impl EmailAttachment {
113 pub fn new(
115 filename: impl Into<String>,
116 content_type: impl Into<String>,
117 content: Vec<u8>,
118 ) -> Self {
119 Self {
120 filename: filename.into(),
121 content_type: content_type.into(),
122 content,
123 content_id: None,
124 inline: false,
125 }
126 }
127
128 pub fn inline(
130 filename: impl Into<String>,
131 content_type: impl Into<String>,
132 content: Vec<u8>,
133 content_id: impl Into<String>,
134 ) -> Self {
135 Self {
136 filename: filename.into(),
137 content_type: content_type.into(),
138 content,
139 content_id: Some(content_id.into()),
140 inline: true,
141 }
142 }
143
144 pub fn size(&self) -> usize {
146 self.content.len()
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct EmailMessage {
153 pub id: String,
155
156 pub from: EmailAddress,
158
159 pub reply_to: Option<EmailAddress>,
161
162 pub recipients: EmailRecipients,
164
165 pub subject: String,
167
168 pub text: Option<String>,
170
171 pub html: Option<String>,
173
174 pub attachments: Vec<EmailAttachment>,
176
177 pub headers: HashMap<String, String>,
179
180 pub template_data: Option<HashMap<String, serde_json::Value>>,
182
183 pub created_at: DateTime<Utc>,
185
186 pub scheduled_at: Option<DateTime<Utc>>,
188
189 pub priority: EmailPriority,
191
192 pub tracking: bool,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct EmailRecipients {
199 pub to: Vec<EmailAddress>,
201
202 pub cc: Vec<EmailAddress>,
204
205 pub bcc: Vec<EmailAddress>,
207}
208
209impl EmailRecipients {
210 pub fn new(to: Vec<EmailAddress>) -> Self {
212 Self {
213 to,
214 cc: Vec::new(),
215 bcc: Vec::new(),
216 }
217 }
218
219 pub fn with_cc_bcc(
221 to: Vec<EmailAddress>,
222 cc: Vec<EmailAddress>,
223 bcc: Vec<EmailAddress>,
224 ) -> Self {
225 Self { to, cc, bcc }
226 }
227
228 pub fn add_to(&mut self, recipient: EmailAddress) {
230 self.to.push(recipient);
231 }
232
233 pub fn add_cc(&mut self, recipient: EmailAddress) {
235 self.cc.push(recipient);
236 }
237
238 pub fn add_bcc(&mut self, recipient: EmailAddress) {
240 self.bcc.push(recipient);
241 }
242
243 pub fn all(&self) -> Vec<&EmailAddress> {
245 self.to.iter()
246 .chain(self.cc.iter())
247 .chain(self.bcc.iter())
248 .collect()
249 }
250
251 pub fn count(&self) -> usize {
253 self.to.len() + self.cc.len() + self.bcc.len()
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
259pub enum EmailPriority {
260 Low = 1,
262
263 #[default]
265 Normal = 3,
266
267 High = 5,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273pub enum EmailStatus {
274 Pending,
276
277 Sent,
279
280 Failed,
282
283 Bounced,
285
286 Opened,
288
289 Clicked,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct EmailDeliveryReport {
296 pub message_id: String,
298
299 pub status: EmailStatus,
301
302 pub provider_message_id: Option<String>,
304
305 pub delivered_at: Option<DateTime<Utc>>,
307
308 pub error: Option<String>,
310
311 pub bounce_reason: Option<String>,
313
314 pub opened_at: Option<DateTime<Utc>>,
316
317 pub clicked_at: Vec<DateTime<Utc>>,
319
320 pub metadata: HashMap<String, String>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct EmailTemplate {
327 pub name: String,
329
330 pub subject: String,
332
333 pub html_template: String,
335
336 pub text_template: Option<String>,
338
339 pub description: Option<String>,
341
342 pub variables: Option<HashMap<String, String>>,
344}
345
346impl EmailMessage {
347 pub fn builder() -> EmailMessageBuilder {
349 EmailMessageBuilder::new()
350 }
351
352 pub fn validate(&self) -> Result<(), String> {
354 if !self.from.is_valid() {
355 return Err("Invalid sender email address".to_string());
356 }
357
358 if self.recipients.to.is_empty() {
359 return Err("At least one 'to' recipient is required".to_string());
360 }
361
362 for recipient in self.recipients.all() {
363 if !recipient.is_valid() {
364 return Err(format!("Invalid recipient email address: {}", recipient.email));
365 }
366 }
367
368 if self.subject.is_empty() {
369 return Err("Email subject cannot be empty".to_string());
370 }
371
372 if self.text.is_none() && self.html.is_none() {
373 return Err("Email must have either text or HTML content".to_string());
374 }
375
376 Ok(())
377 }
378
379 pub fn primary_content(&self) -> Option<&str> {
381 self.html.as_deref().or(self.text.as_deref())
382 }
383
384 pub fn has_attachments(&self) -> bool {
386 !self.attachments.is_empty()
387 }
388
389 pub fn attachment_size(&self) -> usize {
391 self.attachments.iter().map(|a| a.size()).sum()
392 }
393}
394
395pub struct EmailMessageBuilder {
397 message: EmailMessage,
398}
399
400impl EmailMessageBuilder {
401 pub fn new() -> Self {
403 Self {
404 message: EmailMessage {
405 id: Uuid::new_v4().to_string(),
406 from: EmailAddress::new(""),
407 reply_to: None,
408 recipients: EmailRecipients::new(Vec::new()),
409 subject: String::new(),
410 text: None,
411 html: None,
412 attachments: Vec::new(),
413 headers: HashMap::new(),
414 template_data: None,
415 created_at: Utc::now(),
416 scheduled_at: None,
417 priority: EmailPriority::Normal,
418 tracking: false,
419 },
420 }
421 }
422
423 pub fn from(mut self, from: impl Into<EmailAddress>) -> Self {
425 self.message.from = from.into();
426 self
427 }
428
429 pub fn reply_to(mut self, reply_to: impl Into<EmailAddress>) -> Self {
431 self.message.reply_to = Some(reply_to.into());
432 self
433 }
434
435 pub fn to(mut self, to: impl Into<EmailAddress>) -> Self {
437 self.message.recipients.to.push(to.into());
438 self
439 }
440
441 pub fn to_many(mut self, to: Vec<EmailAddress>) -> Self {
443 self.message.recipients.to.extend(to);
444 self
445 }
446
447 pub fn cc(mut self, cc: impl Into<EmailAddress>) -> Self {
449 self.message.recipients.cc.push(cc.into());
450 self
451 }
452
453 pub fn bcc(mut self, bcc: impl Into<EmailAddress>) -> Self {
455 self.message.recipients.bcc.push(bcc.into());
456 self
457 }
458
459 pub fn subject(mut self, subject: impl Into<String>) -> Self {
461 self.message.subject = subject.into();
462 self
463 }
464
465 pub fn text(mut self, text: impl Into<String>) -> Self {
467 self.message.text = Some(text.into());
468 self
469 }
470
471 pub fn html(mut self, html: impl Into<String>) -> Self {
473 self.message.html = Some(html.into());
474 self
475 }
476
477 pub fn attachment(mut self, attachment: EmailAttachment) -> Self {
479 self.message.attachments.push(attachment);
480 self
481 }
482
483 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
485 self.message.headers.insert(key.into(), value.into());
486 self
487 }
488
489 pub fn template_data(mut self, data: HashMap<String, serde_json::Value>) -> Self {
491 self.message.template_data = Some(data);
492 self
493 }
494
495 pub fn scheduled_at(mut self, time: DateTime<Utc>) -> Self {
497 self.message.scheduled_at = Some(time);
498 self
499 }
500
501 pub fn priority(mut self, priority: EmailPriority) -> Self {
503 self.message.priority = priority;
504 self
505 }
506
507 pub fn tracking(mut self, enabled: bool) -> Self {
509 self.message.tracking = enabled;
510 self
511 }
512
513 pub fn build(self) -> EmailMessage {
515 self.message
516 }
517}
518
519impl Default for EmailMessageBuilder {
520 fn default() -> Self {
521 Self::new()
522 }
523}