acton_htmx/email/backend/
console.rs

1//! Console backend for development
2//!
3//! Prints emails to the console instead of sending them.
4//! Useful for development and testing.
5
6use async_trait::async_trait;
7use tracing::{debug, info};
8
9use crate::email::{Email, EmailError, EmailSender};
10
11/// Console email backend for development
12///
13/// Logs emails to the console instead of sending them.
14/// Useful for development and testing without needing SMTP or AWS credentials.
15///
16/// # Examples
17///
18/// ```rust
19/// use acton_htmx::email::{Email, EmailSender, ConsoleBackend};
20///
21/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22/// let backend = ConsoleBackend::new();
23///
24/// let email = Email::new()
25///     .to("user@example.com")
26///     .from("noreply@myapp.com")
27///     .subject("Hello!")
28///     .text("Hello, World!");
29///
30/// backend.send(email).await?; // Prints to console
31/// # Ok(())
32/// # }
33/// ```
34#[derive(Debug, Clone, Default)]
35pub struct ConsoleBackend {
36    /// Whether to log email content in debug mode
37    verbose: bool,
38}
39
40impl ConsoleBackend {
41    /// Create a new console backend
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Create a verbose console backend that logs full email content
48    #[must_use]
49    pub const fn verbose() -> Self {
50        Self { verbose: true }
51    }
52}
53
54#[async_trait]
55impl EmailSender for ConsoleBackend {
56    async fn send(&self, email: Email) -> Result<(), EmailError> {
57        // Validate email first
58        email.validate()?;
59
60        let from = email.from.as_ref().ok_or(EmailError::NoSender)?;
61        let subject = email.subject.as_ref().ok_or(EmailError::NoSubject)?;
62
63        // Log email metadata
64        info!(
65            from = %from,
66            to = ?email.to,
67            cc = ?email.cc,
68            bcc = ?email.bcc,
69            subject = %subject,
70            "Console email sent"
71        );
72
73        if self.verbose {
74            debug!(
75                reply_to = ?email.reply_to,
76                has_html = email.html.is_some(),
77                has_text = email.text.is_some(),
78                headers = ?email.headers,
79                "Email details"
80            );
81
82            if let Some(text) = &email.text {
83                debug!(text = %text, "Email text content");
84            }
85
86            if let Some(html) = &email.html {
87                debug!(html = %html, "Email HTML content");
88            }
89        }
90
91        // Also print to stdout for visibility in development
92        println!("\n╭─────────────────────────────────────────────────────╮");
93        println!("│ 📧 Console Email                                     │");
94        println!("├─────────────────────────────────────────────────────┤");
95        println!("│ From:    {from:<43} │");
96        println!("│ To:      {:<43} │", email.to.join(", "));
97        if !email.cc.is_empty() {
98            println!("│ CC:      {:<43} │", email.cc.join(", "));
99        }
100        if !email.bcc.is_empty() {
101            println!("│ BCC:     {:<43} │", email.bcc.join(", "));
102        }
103        if let Some(reply_to) = &email.reply_to {
104            println!("│ Reply-To: {reply_to:<42} │");
105        }
106        println!("│ Subject: {subject:<43} │");
107        println!("├─────────────────────────────────────────────────────┤");
108
109        if let Some(text) = &email.text {
110            println!("│ Plain Text Content:                                 │");
111            println!("├─────────────────────────────────────────────────────┤");
112            for line in text.lines() {
113                let truncated = if line.len() > 51 {
114                    format!("{}...", &line[..48])
115                } else {
116                    line.to_string()
117                };
118                println!("│ {truncated:<51} │");
119            }
120            println!("├─────────────────────────────────────────────────────┤");
121        }
122
123        if let Some(html) = &email.html {
124            println!("│ HTML Content:                                       │");
125            println!("├─────────────────────────────────────────────────────┤");
126            for line in html.lines().take(5) {
127                let truncated = if line.len() > 51 {
128                    format!("{}...", &line[..48])
129                } else {
130                    line.to_string()
131                };
132                println!("│ {truncated:<51} │");
133            }
134            if html.lines().count() > 5 {
135                println!("│ ... (truncated)                                     │");
136            }
137            println!("├─────────────────────────────────────────────────────┤");
138        }
139
140        println!("╰─────────────────────────────────────────────────────╯\n");
141
142        Ok(())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[tokio::test]
151    async fn test_console_backend_send() {
152        let backend = ConsoleBackend::new();
153
154        let email = Email::new()
155            .to("user@example.com")
156            .from("noreply@myapp.com")
157            .subject("Test Email")
158            .text("This is a test email");
159
160        let result = backend.send(email).await;
161        assert!(result.is_ok());
162    }
163
164    #[tokio::test]
165    async fn test_console_backend_verbose() {
166        let backend = ConsoleBackend::verbose();
167
168        let email = Email::new()
169            .to("user@example.com")
170            .from("noreply@myapp.com")
171            .subject("Test Email")
172            .text("This is plain text")
173            .html("<h1>This is HTML</h1>");
174
175        let result = backend.send(email).await;
176        assert!(result.is_ok());
177    }
178
179    #[tokio::test]
180    async fn test_console_backend_with_cc_and_bcc() {
181        let backend = ConsoleBackend::new();
182
183        let email = Email::new()
184            .to("user@example.com")
185            .cc("cc@example.com")
186            .bcc("bcc@example.com")
187            .from("noreply@myapp.com")
188            .subject("Test Email")
189            .text("Test content");
190
191        let result = backend.send(email).await;
192        assert!(result.is_ok());
193    }
194}