Skip to main content

forge_core/email/
mod.rs

1//! Email sending trait and types.
2//!
3//! Defines the `EmailSender` trait used by handler contexts via `ctx.email()`.
4//! The runtime provides concrete implementations (SMTP, HTTP-based providers).
5
6use std::future::Future;
7use std::pin::Pin;
8
9use crate::error::Result;
10
11/// An email message.
12#[derive(Debug, Clone)]
13pub struct Email {
14    /// Overrides the default `from` in config if set.
15    pub from: Option<String>,
16    pub to: Vec<String>,
17    pub cc: Vec<String>,
18    pub bcc: Vec<String>,
19    pub subject: String,
20    pub text: Option<String>,
21    pub html: Option<String>,
22    pub reply_to: Option<String>,
23}
24
25impl Email {
26    /// Create a new email to a single recipient.
27    pub fn to(recipient: impl Into<String>) -> EmailBuilder {
28        EmailBuilder {
29            email: Self {
30                from: None,
31                to: vec![recipient.into()],
32                cc: Vec::new(),
33                bcc: Vec::new(),
34                subject: String::new(),
35                text: None,
36                html: None,
37                reply_to: None,
38            },
39        }
40    }
41}
42
43/// Builder for constructing email messages.
44pub struct EmailBuilder {
45    email: Email,
46}
47
48impl EmailBuilder {
49    pub fn to(mut self, recipient: impl Into<String>) -> Self {
50        self.email.to.push(recipient.into());
51        self
52    }
53
54    pub fn from(mut self, sender: impl Into<String>) -> Self {
55        self.email.from = Some(sender.into());
56        self
57    }
58
59    pub fn cc(mut self, recipient: impl Into<String>) -> Self {
60        self.email.cc.push(recipient.into());
61        self
62    }
63
64    pub fn bcc(mut self, recipient: impl Into<String>) -> Self {
65        self.email.bcc.push(recipient.into());
66        self
67    }
68
69    pub fn subject(mut self, subject: impl Into<String>) -> Self {
70        self.email.subject = subject.into();
71        self
72    }
73
74    pub fn text(mut self, body: impl Into<String>) -> Self {
75        self.email.text = Some(body.into());
76        self
77    }
78
79    pub fn html(mut self, body: impl Into<String>) -> Self {
80        self.email.html = Some(body.into());
81        self
82    }
83
84    pub fn reply_to(mut self, address: impl Into<String>) -> Self {
85        self.email.reply_to = Some(address.into());
86        self
87    }
88
89    pub fn build(self) -> Email {
90        self.email
91    }
92}
93
94/// Trait for sending emails from handler contexts.
95///
96/// Implemented by the runtime for SMTP and HTTP-based providers (Resend, SES).
97/// Mocked in test contexts.
98pub trait EmailSender: Send + Sync + 'static {
99    /// Send an email. Returns the provider's message ID on success.
100    fn send<'a>(
101        &'a self,
102        email: &'a Email,
103    ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
104}
105
106/// Email configuration from forge.toml.
107#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
108#[serde(default)]
109pub struct EmailConfig {
110    pub enabled: bool,
111    /// Provider: "smtp", "resend", "ses", "log" (development).
112    pub provider: String,
113    /// Default sender address.
114    pub from: String,
115    pub smtp_host: Option<String>,
116    /// Default 587.
117    pub smtp_port: Option<u16>,
118    /// Env var containing the API key or SMTP password.
119    pub secret_env: Option<String>,
120}
121
122#[cfg(test)]
123#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn email_builder_creates_message() {
129        let email = Email::to("user@example.com")
130            .from("noreply@app.com")
131            .subject("Hello")
132            .text("Hi there")
133            .html("<h1>Hi there</h1>")
134            .cc("cc@example.com")
135            .bcc("bcc@example.com")
136            .reply_to("reply@app.com")
137            .build();
138
139        assert_eq!(email.to, vec!["user@example.com"]);
140        assert_eq!(email.from.as_deref(), Some("noreply@app.com"));
141        assert_eq!(email.subject, "Hello");
142        assert_eq!(email.text.as_deref(), Some("Hi there"));
143        assert_eq!(email.html.as_deref(), Some("<h1>Hi there</h1>"));
144        assert_eq!(email.cc, vec!["cc@example.com"]);
145        assert_eq!(email.bcc, vec!["bcc@example.com"]);
146        assert_eq!(email.reply_to.as_deref(), Some("reply@app.com"));
147    }
148
149    #[test]
150    fn email_builder_multiple_recipients() {
151        let email = Email::to("a@example.com")
152            .to("b@example.com")
153            .subject("Test")
154            .build();
155
156        assert_eq!(email.to.len(), 2);
157    }
158}