1use std::future::Future;
7use std::pin::Pin;
8
9use crate::error::Result;
10
11#[derive(Debug, Clone)]
13pub struct Email {
14 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 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
43pub 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
94pub trait EmailSender: Send + Sync + 'static {
99 fn send<'a>(
101 &'a self,
102 email: &'a Email,
103 ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
104}
105
106#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
108#[serde(default)]
109pub struct EmailConfig {
110 pub enabled: bool,
111 pub provider: String,
113 pub from: String,
115 pub smtp_host: Option<String>,
116 pub smtp_port: Option<u16>,
118 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}