elif_email/
lib.rs

1//! # elif-email
2//! 
3//! Email system for elif.rs with multiple providers, templating, and background queuing.
4//! 
5//! ## Features
6//! 
7//! - Multiple email providers (SMTP, SendGrid, Mailgun)
8//! - Handlebars template system with layouts
9//! - Background email queuing
10//! - Email tracking and analytics
11//! - Type-safe email composition with Mailable trait
12
13pub mod config;
14pub mod error;
15pub mod providers;
16pub mod templates;
17pub mod tracking;
18pub mod mailable;
19pub mod validation;
20pub mod compression;
21pub mod queue;
22
23#[cfg(feature = "integration-examples")]
24pub mod integration_example;
25
26pub use config::*;
27pub use error::*;
28pub use mailable::*;
29pub use providers::*;
30#[cfg(test)]
31pub use providers::{MockEmailProvider, PanickingEmailProvider};
32pub use templates::*;
33pub use tracking::*;
34pub use validation::*;
35pub use compression::*;
36pub use queue::*;
37
38use async_trait::async_trait;
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41use uuid::Uuid;
42
43/// Core email message structure
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Email {
46    /// Unique identifier for tracking
47    pub id: Uuid,
48    /// Sender email address
49    pub from: String,
50    /// Recipient email addresses
51    pub to: Vec<String>,
52    /// CC recipients
53    pub cc: Option<Vec<String>>,
54    /// BCC recipients
55    pub bcc: Option<Vec<String>>,
56    /// Reply-to address
57    pub reply_to: Option<String>,
58    /// Email subject
59    pub subject: String,
60    /// HTML body content
61    pub html_body: Option<String>,
62    /// Plain text body content
63    pub text_body: Option<String>,
64    /// Email attachments
65    pub attachments: Vec<Attachment>,
66    /// Email headers
67    pub headers: HashMap<String, String>,
68    /// Tracking metadata
69    pub tracking: TrackingOptions,
70}
71
72/// Email attachment
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Attachment {
75    /// Filename
76    pub filename: String,
77    /// MIME content type
78    pub content_type: String,
79    /// Binary content
80    pub content: Vec<u8>,
81    /// Inline attachment ID for embedding in HTML
82    pub content_id: Option<String>,
83    /// Attachment disposition (attachment or inline)
84    pub disposition: AttachmentDisposition,
85    /// Attachment size in bytes
86    pub size: usize,
87    /// Whether content is compressed
88    pub compressed: bool,
89}
90
91/// Attachment disposition type
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub enum AttachmentDisposition {
94    /// Regular file attachment
95    Attachment,
96    /// Inline attachment (e.g., embedded image)
97    Inline,
98}
99
100/// Email tracking configuration
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct TrackingOptions {
103    /// Enable open tracking
104    pub track_opens: bool,
105    /// Enable click tracking
106    pub track_clicks: bool,
107    /// Custom tracking parameters
108    pub custom_params: HashMap<String, String>,
109}
110
111impl Default for TrackingOptions {
112    fn default() -> Self {
113        Self {
114            track_opens: false,
115            track_clicks: false,
116            custom_params: HashMap::new(),
117        }
118    }
119}
120
121/// Email provider abstraction
122#[async_trait]
123pub trait EmailProvider: Send + Sync {
124    /// Send an email immediately
125    async fn send(&self, email: &Email) -> Result<EmailResult, EmailError>;
126    
127    /// Validate configuration
128    async fn validate_config(&self) -> Result<(), EmailError>;
129    
130    /// Get provider name
131    fn provider_name(&self) -> &'static str;
132}
133
134/// Email sending result
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct EmailResult {
137    /// Email ID
138    pub email_id: Uuid,
139    /// Provider-specific message ID
140    pub message_id: String,
141    /// Send timestamp
142    pub sent_at: chrono::DateTime<chrono::Utc>,
143    /// Provider name
144    pub provider: String,
145}
146
147impl Email {
148    /// Create a new email
149    pub fn new() -> Self {
150        Self {
151            id: Uuid::new_v4(),
152            from: String::new(),
153            to: Vec::new(),
154            cc: None,
155            bcc: None,
156            reply_to: None,
157            subject: String::new(),
158            html_body: None,
159            text_body: None,
160            attachments: Vec::new(),
161            headers: HashMap::new(),
162            tracking: TrackingOptions::default(),
163        }
164    }
165
166    /// Set sender
167    pub fn from(mut self, from: impl Into<String>) -> Self {
168        self.from = from.into();
169        self
170    }
171
172    /// Add recipient
173    pub fn to(mut self, to: impl Into<String>) -> Self {
174        self.to.push(to.into());
175        self
176    }
177
178    /// Set subject
179    pub fn subject(mut self, subject: impl Into<String>) -> Self {
180        self.subject = subject.into();
181        self
182    }
183
184    /// Set HTML body
185    pub fn html_body(mut self, html: impl Into<String>) -> Self {
186        self.html_body = Some(html.into());
187        self
188    }
189
190    /// Set text body
191    pub fn text_body(mut self, text: impl Into<String>) -> Self {
192        self.text_body = Some(text.into());
193        self
194    }
195
196    /// Add attachment
197    pub fn attach(mut self, attachment: Attachment) -> Self {
198        self.attachments.push(attachment);
199        self
200    }
201
202    /// Enable tracking
203    pub fn with_tracking(mut self, track_opens: bool, track_clicks: bool) -> Self {
204        self.tracking.track_opens = track_opens;
205        self.tracking.track_clicks = track_clicks;
206        self
207    }
208    
209    /// Add an inline attachment (for embedding in HTML emails)
210    pub fn attach_inline(mut self, attachment: Attachment) -> Self {
211        self.attachments.push(attachment);
212        self
213    }
214    
215    /// Validate all attachments against configuration
216    pub fn validate_attachments(&self, config: &crate::config::AttachmentConfig) -> Result<(), crate::EmailError> {
217        crate::compression::validate_attachments(&self.attachments, config)
218    }
219    
220    /// Get all inline attachments (for HTML embedding)
221    pub fn inline_attachments(&self) -> Vec<&Attachment> {
222        self.attachments.iter()
223            .filter(|a| matches!(a.disposition, AttachmentDisposition::Inline))
224            .collect()
225    }
226    
227    /// Get all regular attachments
228    pub fn regular_attachments(&self) -> Vec<&Attachment> {
229        self.attachments.iter()
230            .filter(|a| matches!(a.disposition, AttachmentDisposition::Attachment))
231            .collect()
232    }
233    
234    /// Apply compression to all attachments if configured
235    pub fn compress_attachments(&mut self, config: &crate::config::AttachmentConfig) -> Result<(), crate::EmailError> {
236        for attachment in &mut self.attachments {
237            crate::compression::AttachmentCompressor::compress_if_beneficial(attachment, config)?;
238        }
239        Ok(())
240    }
241    
242    /// Get estimated size after compression
243    pub fn estimated_compressed_size(&self) -> usize {
244        self.attachments.iter()
245            .map(|a| {
246                if a.compressed {
247                    a.size
248                } else {
249                    let ratio = crate::compression::AttachmentCompressor::estimate_compression_ratio(a);
250                    (a.size as f64 * ratio) as usize
251                }
252            })
253            .sum()
254    }
255}
256
257impl Default for Email {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263impl Attachment {
264    /// Create a new attachment with automatic MIME type detection
265    pub fn new(filename: impl Into<String>, content: Vec<u8>) -> Self {
266        let filename = filename.into();
267        let content_type = mime_guess::from_path(&filename)
268            .first()
269            .map(|m| m.to_string())
270            .unwrap_or_else(|| "application/octet-stream".to_string());
271        
272        let size = content.len();
273        
274        Self {
275            filename,
276            content_type,
277            content,
278            content_id: None,
279            disposition: AttachmentDisposition::Attachment,
280            size,
281            compressed: false,
282        }
283    }
284    
285    /// Create a new inline attachment (for embedding in HTML)
286    pub fn inline(filename: impl Into<String>, content: Vec<u8>, content_id: impl Into<String>) -> Self {
287        let mut attachment = Self::new(filename, content);
288        attachment.disposition = AttachmentDisposition::Inline;
289        attachment.content_id = Some(content_id.into());
290        attachment
291    }
292    
293    /// Set custom content type
294    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
295        self.content_type = content_type.into();
296        self
297    }
298    
299    /// Set as inline attachment
300    pub fn as_inline(mut self, content_id: impl Into<String>) -> Self {
301        self.disposition = AttachmentDisposition::Inline;
302        self.content_id = Some(content_id.into());
303        self
304    }
305    
306    /// Check if attachment is an image
307    pub fn is_image(&self) -> bool {
308        self.content_type.starts_with("image/")
309    }
310    
311    /// Check if attachment can be compressed
312    pub fn can_compress(&self) -> bool {
313        matches!(self.content_type.as_str(),
314            "image/jpeg" | "image/png" | "image/webp" |
315            "text/plain" | "text/html" | "text/css" | "text/javascript" |
316            "application/json" | "application/xml"
317        )
318    }
319    
320    /// Validate attachment against configuration
321    pub fn validate(&self, config: &crate::config::AttachmentConfig) -> Result<(), crate::EmailError> {
322        // Check size limits
323        if self.size > config.max_size {
324            return Err(crate::EmailError::validation(
325                "attachment_size",
326                format!("Attachment '{}' is too large: {} bytes (max: {} bytes)",
327                    self.filename, self.size, config.max_size)
328            ));
329        }
330        
331        // Check allowed types
332        if !config.allowed_types.is_empty() && !config.allowed_types.contains(&self.content_type) {
333            return Err(crate::EmailError::validation(
334                "attachment_type",
335                format!("Attachment type '{}' is not allowed", self.content_type)
336            ));
337        }
338        
339        // Check blocked types
340        if config.blocked_types.contains(&self.content_type) {
341            return Err(crate::EmailError::validation(
342                "attachment_type",
343                format!("Attachment type '{}' is blocked", self.content_type)
344            ));
345        }
346        
347        Ok(())
348    }
349}
350
351impl Default for AttachmentDisposition {
352    fn default() -> Self {
353        AttachmentDisposition::Attachment
354    }
355}