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