cloudillo_email/
sender.rs1use crate::prelude::*;
9use crate::EmailMessage;
10use cloudillo_core::settings::service::SettingsService;
11use lettre::transport::smtp::authentication::Credentials;
12use lettre::transport::smtp::SmtpTransport;
13use lettre::{Message, Transport};
14use std::sync::Arc;
15use std::time::Duration;
16
17pub struct EmailSender {
19 settings_service: Arc<SettingsService>,
20}
21
22impl EmailSender {
23 pub fn new(settings_service: Arc<SettingsService>) -> ClResult<Self> {
25 Ok(Self { settings_service })
26 }
27
28 pub async fn send(&self, tn_id: TnId, message: EmailMessage) -> ClResult<()> {
30 if !self.settings_service.get_bool(tn_id, "email.enabled").await? {
32 info!("Email sending disabled, skipping send to {}", message.to);
33 return Ok(());
34 }
35
36 let host = match self.settings_service.get_string_opt(tn_id, "email.smtp.host").await? {
38 Some(h) if !h.is_empty() => h,
39 _ => {
40 debug!("SMTP host not configured, silently skipping email to {}", message.to);
41 return Ok(());
42 }
43 };
44
45 let port = u16::try_from(self.settings_service.get_int(tn_id, "email.smtp.port").await?)
47 .unwrap_or_default();
48 let username = self
49 .settings_service
50 .get_string_opt(tn_id, "email.smtp.username")
51 .await?
52 .unwrap_or_default();
53 let password = self
54 .settings_service
55 .get_string_opt(tn_id, "email.smtp.password")
56 .await?
57 .unwrap_or_default();
58 let from_address = self.settings_service.get_string(tn_id, "email.from.address").await?;
59 let from_name = match &message.from_name_override {
61 Some(name) => name.clone(),
62 None => self.settings_service.get_string(tn_id, "email.from.name").await?,
63 };
64 let tls_mode = self.settings_service.get_string(tn_id, "email.smtp.tls_mode").await?;
65 let timeout_seconds = u64::try_from(
66 self.settings_service.get_int(tn_id, "email.smtp.timeout_seconds").await?,
67 )
68 .unwrap_or_default();
69
70 debug!("Sending email to {} via {}:{} with TLS mode: {}", message.to, host, port, tls_mode);
71
72 if !message.to.contains('@') {
74 return Err(Error::ValidationError("Invalid recipient email address".into()));
75 }
76
77 if !from_address.contains('@') {
78 return Err(Error::ValidationError("Invalid from email address".into()));
79 }
80
81 let escaped_name = from_name.replace('\\', "\\\\").replace('"', "\\\"");
84 let email_builder = Message::builder()
85 .from(
86 format!("\"{}\" <{}>", escaped_name, from_address)
87 .parse()
88 .map_err(|_| Error::ValidationError("Invalid from email format".into()))?,
89 )
90 .to(message
91 .to
92 .parse()
93 .map_err(|_| Error::ValidationError("Invalid recipient email format".into()))?)
94 .subject(&message.subject);
95
96 let email = if let Some(html_body) = message.html_body {
97 email_builder
99 .multipart(
100 lettre::message::MultiPart::alternative()
101 .singlepart(lettre::message::SinglePart::plain(message.text_body))
102 .singlepart(lettre::message::SinglePart::html(html_body)),
103 )
104 .map_err(|e| Error::ValidationError(format!("Failed to build email: {}", e)))?
105 } else {
106 email_builder
108 .singlepart(lettre::message::SinglePart::plain(message.text_body))
109 .map_err(|e| Error::ValidationError(format!("Failed to build email: {}", e)))?
110 };
111
112 let tls = match tls_mode.as_str() {
114 "tls" => {
115 debug!("Using TLS mode");
116 lettre::transport::smtp::client::Tls::Wrapper(
117 lettre::transport::smtp::client::TlsParameters::builder(host.clone())
118 .build()
119 .map_err(|e| Error::ConfigError(format!("TLS configuration error: {}", e)))?,
120 )
121 }
122 "starttls" => {
123 debug!("Using STARTTLS mode");
124 lettre::transport::smtp::client::Tls::Opportunistic(
125 lettre::transport::smtp::client::TlsParameters::builder(host.clone())
126 .build()
127 .map_err(|e| Error::ConfigError(format!("TLS configuration error: {}", e)))?,
128 )
129 }
130 "none" => {
131 debug!("No TLS mode");
132 lettre::transport::smtp::client::Tls::None
133 }
134 _ => {
135 return Err(Error::ConfigError(format!(
136 "Invalid TLS mode: {}. Must be 'none', 'starttls', or 'tls'",
137 tls_mode
138 )))
139 }
140 };
141
142 let credentials = Credentials::new(username, password);
144 let mailer = SmtpTransport::builder_dangerous(&host)
145 .port(port)
146 .timeout(Some(Duration::from_secs(timeout_seconds)))
147 .tls(tls)
148 .credentials(credentials)
149 .build();
150
151 match mailer.send(&email) {
153 Ok(response) => {
154 info!("Email sent successfully to {} (response: {:?})", message.to, response);
155 Ok(())
156 }
157 Err(e) => {
158 warn!("Failed to send email to {}: {}", message.to, e);
159 Err(Error::ServiceUnavailable(format!("SMTP send failed: {}", e)))
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_email_message_creation() {
171 let message = EmailMessage {
172 to: "user@example.com".to_string(),
173 subject: "Test Email".to_string(),
174 text_body: "This is a test".to_string(),
175 html_body: Some("<p>This is a test</p>".to_string()),
176 from_name_override: None,
177 };
178
179 assert_eq!(message.to, "user@example.com");
180 assert_eq!(message.subject, "Test Email");
181 assert!(message.html_body.is_some());
182 }
183
184 #[test]
185 fn test_email_address_validation() {
186 let valid = "user@example.com";
188 assert!(valid.contains('@'));
189
190 let invalid1 = "userexample.com";
192 assert!(!invalid1.contains('@'));
193
194 let invalid2 = "user@";
195 assert!(invalid2.contains('@'));
196 }
197}
198
199