Skip to main content

backbone_email/
types.rs

1//! Email types and structures
2
3use serde::{Serialize, Deserialize};
4use std::collections::HashMap;
5use chrono::{DateTime, Utc};
6use uuid::Uuid;
7
8/// Email address representation
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct EmailAddress {
11    /// Email address (e.g., "user@example.com")
12    pub email: String,
13
14    /// Display name (optional, e.g., "John Doe")
15    pub name: Option<String>,
16}
17
18impl EmailAddress {
19    /// Create new email address
20    /// Parses both "user@example.com" and "Name <user@example.com>" formats
21    pub fn new(email: impl Into<String>) -> Self {
22        let email_str = email.into();
23        Self::parse(&email_str)
24    }
25
26    /// Parse email string, supporting both "user@example.com" and "Name <user@example.com>" formats
27    fn parse(email_str: &str) -> Self {
28        let trimmed = email_str.trim();
29
30        // Check for "Name <email>" format
31        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        // Simple email format
46        Self {
47            email: trimmed.to_string(),
48            name: None,
49        }
50    }
51
52    /// Create email address with display name
53    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    /// Validate email format (basic validation)
61    pub fn is_valid(&self) -> bool {
62        self.email.contains('@') && self.email.contains('.')
63    }
64
65    /// Get email as string format
66    #[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/// Email attachment
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EmailAttachment {
96    /// File name
97    pub filename: String,
98
99    /// Content type (MIME type)
100    pub content_type: String,
101
102    /// File content (base64 encoded)
103    pub content: Vec<u8>,
104
105    /// Content ID for inline attachments
106    pub content_id: Option<String>,
107
108    /// Whether this is an inline attachment
109    pub inline: bool,
110}
111
112impl EmailAttachment {
113    /// Create new attachment from bytes
114    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    /// Create inline attachment
129    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    /// Get file size in bytes
145    pub fn size(&self) -> usize {
146        self.content.len()
147    }
148}
149
150/// Email message
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct EmailMessage {
153    /// Unique message ID
154    pub id: String,
155
156    /// Sender email address
157    pub from: EmailAddress,
158
159    /// Reply-to email address (optional)
160    pub reply_to: Option<EmailAddress>,
161
162    /// Recipients (to, cc, bcc)
163    pub recipients: EmailRecipients,
164
165    /// Email subject
166    pub subject: String,
167
168    /// Plain text content
169    pub text: Option<String>,
170
171    /// HTML content
172    pub html: Option<String>,
173
174    /// Attachments
175    pub attachments: Vec<EmailAttachment>,
176
177    /// Custom headers
178    pub headers: HashMap<String, String>,
179
180    /// Template variables (for template rendering)
181    pub template_data: Option<HashMap<String, serde_json::Value>>,
182
183    /// Message creation timestamp
184    pub created_at: DateTime<Utc>,
185
186    /// Scheduled send time (optional)
187    pub scheduled_at: Option<DateTime<Utc>>,
188
189    /// Priority level
190    pub priority: EmailPriority,
191
192    /// Whether to track opens and clicks
193    pub tracking: bool,
194}
195
196/// Email recipients grouped by type
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct EmailRecipients {
199    /// To recipients
200    pub to: Vec<EmailAddress>,
201
202    /// CC recipients (carbon copy)
203    pub cc: Vec<EmailAddress>,
204
205    /// BCC recipients (blind carbon copy)
206    pub bcc: Vec<EmailAddress>,
207}
208
209impl EmailRecipients {
210    /// Create new recipients with only 'to' addresses
211    pub fn new(to: Vec<EmailAddress>) -> Self {
212        Self {
213            to,
214            cc: Vec::new(),
215            bcc: Vec::new(),
216        }
217    }
218
219    /// Create recipients with to, cc, and bcc
220    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    /// Add 'to' recipient
229    pub fn add_to(&mut self, recipient: EmailAddress) {
230        self.to.push(recipient);
231    }
232
233    /// Add CC recipient
234    pub fn add_cc(&mut self, recipient: EmailAddress) {
235        self.cc.push(recipient);
236    }
237
238    /// Add BCC recipient
239    pub fn add_bcc(&mut self, recipient: EmailAddress) {
240        self.bcc.push(recipient);
241    }
242
243    /// Get all recipients
244    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    /// Get total recipient count
252    pub fn count(&self) -> usize {
253        self.to.len() + self.cc.len() + self.bcc.len()
254    }
255}
256
257/// Email priority levels
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
259pub enum EmailPriority {
260    /// Low priority
261    Low = 1,
262
263    /// Normal priority (default)
264    #[default]
265    Normal = 3,
266
267    /// High priority
268    High = 5,
269}
270
271/// Email delivery status
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273pub enum EmailStatus {
274    /// Pending to be sent
275    Pending,
276
277    /// Successfully sent
278    Sent,
279
280    /// Delivery failed
281    Failed,
282
283    /// Bounced (returned by recipient server)
284    Bounced,
285
286    /// Opened by recipient
287    Opened,
288
289    /// Clicked (for tracking emails)
290    Clicked,
291}
292
293/// Email delivery report
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct EmailDeliveryReport {
296    /// Message ID
297    pub message_id: String,
298
299    /// Delivery status
300    pub status: EmailStatus,
301
302    /// Provider-specific message ID
303    pub provider_message_id: Option<String>,
304
305    /// Delivery timestamp
306    pub delivered_at: Option<DateTime<Utc>>,
307
308    /// Error message (if failed)
309    pub error: Option<String>,
310
311    /// Bounce reason (if bounced)
312    pub bounce_reason: Option<String>,
313
314    /// Open tracking timestamp
315    pub opened_at: Option<DateTime<Utc>>,
316
317    /// Click tracking timestamps
318    pub clicked_at: Vec<DateTime<Utc>>,
319
320    /// Additional metadata
321    pub metadata: HashMap<String, String>,
322}
323
324/// Email template
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct EmailTemplate {
327    /// Template name
328    pub name: String,
329
330    /// Template subject (can include template variables)
331    pub subject: String,
332
333    /// HTML template content
334    pub html_template: String,
335
336    /// Plain text template content
337    pub text_template: Option<String>,
338
339    /// Template description
340    pub description: Option<String>,
341
342    /// Template variables schema
343    pub variables: Option<HashMap<String, String>>,
344}
345
346impl EmailMessage {
347    /// Create new email message builder
348    pub fn builder() -> EmailMessageBuilder {
349        EmailMessageBuilder::new()
350    }
351
352    /// Validate email message
353    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    /// Get primary content (HTML preferred, fallback to text)
380    pub fn primary_content(&self) -> Option<&str> {
381        self.html.as_deref().or(self.text.as_deref())
382    }
383
384    /// Check if message has attachments
385    pub fn has_attachments(&self) -> bool {
386        !self.attachments.is_empty()
387    }
388
389    /// Get total attachment size
390    pub fn attachment_size(&self) -> usize {
391        self.attachments.iter().map(|a| a.size()).sum()
392    }
393}
394
395/// Email message builder
396pub struct EmailMessageBuilder {
397    message: EmailMessage,
398}
399
400impl EmailMessageBuilder {
401    /// Create new builder
402    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    /// Set sender
424    pub fn from(mut self, from: impl Into<EmailAddress>) -> Self {
425        self.message.from = from.into();
426        self
427    }
428
429    /// Set reply-to
430    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    /// Add 'to' recipient
436    pub fn to(mut self, to: impl Into<EmailAddress>) -> Self {
437        self.message.recipients.to.push(to.into());
438        self
439    }
440
441    /// Add multiple 'to' recipients
442    pub fn to_many(mut self, to: Vec<EmailAddress>) -> Self {
443        self.message.recipients.to.extend(to);
444        self
445    }
446
447    /// Add CC recipient
448    pub fn cc(mut self, cc: impl Into<EmailAddress>) -> Self {
449        self.message.recipients.cc.push(cc.into());
450        self
451    }
452
453    /// Add BCC recipient
454    pub fn bcc(mut self, bcc: impl Into<EmailAddress>) -> Self {
455        self.message.recipients.bcc.push(bcc.into());
456        self
457    }
458
459    /// Set subject
460    pub fn subject(mut self, subject: impl Into<String>) -> Self {
461        self.message.subject = subject.into();
462        self
463    }
464
465    /// Set text content
466    pub fn text(mut self, text: impl Into<String>) -> Self {
467        self.message.text = Some(text.into());
468        self
469    }
470
471    /// Set HTML content
472    pub fn html(mut self, html: impl Into<String>) -> Self {
473        self.message.html = Some(html.into());
474        self
475    }
476
477    /// Add attachment
478    pub fn attachment(mut self, attachment: EmailAttachment) -> Self {
479        self.message.attachments.push(attachment);
480        self
481    }
482
483    /// Add custom header
484    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    /// Set template data
490    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    /// Set scheduled send time
496    pub fn scheduled_at(mut self, time: DateTime<Utc>) -> Self {
497        self.message.scheduled_at = Some(time);
498        self
499    }
500
501    /// Set priority
502    pub fn priority(mut self, priority: EmailPriority) -> Self {
503        self.message.priority = priority;
504        self
505    }
506
507    /// Enable/disable tracking
508    pub fn tracking(mut self, enabled: bool) -> Self {
509        self.message.tracking = enabled;
510        self
511    }
512
513    /// Build the email message
514    pub fn build(self) -> EmailMessage {
515        self.message
516    }
517}
518
519impl Default for EmailMessageBuilder {
520    fn default() -> Self {
521        Self::new()
522    }
523}