koprogo_api/infrastructure/
email.rs1use lettre::message::{header::ContentType, Mailbox};
2use lettre::transport::smtp::authentication::Credentials;
3use lettre::{Message, SmtpTransport, Transport};
4use std::env;
5use uuid::Uuid;
6
7#[derive(Clone)]
9pub struct EmailService {
10 smtp_host: String,
11 smtp_port: u16,
12 smtp_username: String,
13 smtp_password: String,
14 from_email: String,
15 from_name: String,
16 enabled: bool,
17}
18
19impl EmailService {
20 pub fn from_env() -> Result<Self, String> {
22 let enabled = env::var("SMTP_ENABLED")
23 .unwrap_or_else(|_| "false".to_string())
24 .to_lowercase()
25 .parse::<bool>()
26 .unwrap_or(false);
27
28 if !enabled {
29 log::info!("Email service disabled (SMTP_ENABLED=false)");
30 return Ok(Self {
31 smtp_host: String::new(),
32 smtp_port: 0,
33 smtp_username: String::new(),
34 smtp_password: String::new(),
35 from_email: String::new(),
36 from_name: String::new(),
37 enabled: false,
38 });
39 }
40
41 let smtp_host = env::var("SMTP_HOST").map_err(|_| "SMTP_HOST not set".to_string())?;
42 let smtp_port = env::var("SMTP_PORT")
43 .unwrap_or_else(|_| "587".to_string())
44 .parse::<u16>()
45 .map_err(|_| "SMTP_PORT must be a valid port number".to_string())?;
46 let smtp_username =
47 env::var("SMTP_USERNAME").map_err(|_| "SMTP_USERNAME not set".to_string())?;
48 let smtp_password =
49 env::var("SMTP_PASSWORD").map_err(|_| "SMTP_PASSWORD not set".to_string())?;
50 let from_email =
51 env::var("SMTP_FROM_EMAIL").map_err(|_| "SMTP_FROM_EMAIL not set".to_string())?;
52 let from_name = env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "KoproGo".to_string());
53
54 log::info!("Email service enabled: {}:{}", smtp_host, smtp_port);
55
56 Ok(Self {
57 smtp_host,
58 smtp_port,
59 smtp_username,
60 smtp_password,
61 from_email,
62 from_name,
63 enabled: true,
64 })
65 }
66
67 pub async fn send_gdpr_export_notification(
69 &self,
70 user_email: &str,
71 user_name: &str,
72 export_id: Uuid,
73 ) -> Result<(), String> {
74 if !self.enabled {
75 log::debug!(
76 "Email disabled - would send export notification to {}",
77 user_email
78 );
79 return Ok(());
80 }
81
82 let subject = "Your GDPR Data Export is Ready";
83 let body = format!(
84 r#"Dear {},
85
86Your request to export your personal data has been completed.
87
88Export ID: {}
89Export Date: {}
90
91This export contains all personal information we hold about you, in compliance with GDPR Article 15 (Right to Access).
92
93Important Security Notice:
94- This export contains sensitive personal information
95- Please store it securely and do not share it
96- The export will be available for download for 30 days
97
98If you did not request this export, please contact our Data Protection Officer immediately.
99
100Best regards,
101The KoproGo Team
102
103---
104This is an automated message. Please do not reply to this email.
105For questions, contact: dpo@koprogo.com
106"#,
107 user_name,
108 export_id,
109 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
110 );
111
112 self.send_email(user_email, subject, &body).await
113 }
114
115 pub async fn send_gdpr_erasure_notification(
117 &self,
118 user_email: &str,
119 user_name: &str,
120 owners_anonymized: usize,
121 ) -> Result<(), String> {
122 if !self.enabled {
123 log::debug!(
124 "Email disabled - would send erasure notification to {}",
125 user_email
126 );
127 return Ok(());
128 }
129
130 let subject = "GDPR Data Erasure Confirmation";
131 let body = format!(
132 r#"Dear {},
133
134Your request to erase your personal data has been completed.
135
136Erasure Summary:
137- User account: Anonymized
138- Owner profiles: {} anonymized
139- Erasure Date: {}
140
141In compliance with GDPR Article 17 (Right to Erasure), we have anonymized your personal information.
142
143Important Information:
144- Your account can no longer be recovered
145- Some data may be retained for legal compliance (e.g., financial records for 7 years)
146- Anonymous data may be retained for statistical purposes
147
148If you did not request this erasure, please contact us immediately as this action cannot be reversed.
149
150Best regards,
151The KoproGo Team
152
153---
154This is an automated message. Please do not reply to this email.
155For questions, contact: dpo@koprogo.com
156"#,
157 user_name,
158 owners_anonymized,
159 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
160 );
161
162 self.send_email(user_email, subject, &body).await
163 }
164
165 pub async fn send_admin_gdpr_notification(
167 &self,
168 user_email: &str,
169 user_name: &str,
170 operation: &str,
171 admin_email: &str,
172 ) -> Result<(), String> {
173 if !self.enabled {
174 log::debug!(
175 "Email disabled - would send admin notification to {}",
176 user_email
177 );
178 return Ok(());
179 }
180
181 let subject = format!("GDPR {} Performed by Administrator", operation);
182 let body_text = format!(
183 r#"Dear {},
184
185An administrator has performed a GDPR {} operation on your account.
186
187Operation: {}
188Performed by: {}
189Date: {}
190
191This operation was performed by a KoproGo administrator, typically in response to:
192- A compliance request
193- Legal obligation
194- Account cleanup
195- Data subject request via alternative channel
196
197If you have questions about this operation, please contact our Data Protection Officer.
198
199Best regards,
200The KoproGo Team
201
202---
203This is an automated message. Please do not reply to this email.
204For questions, contact: dpo@koprogo.com
205"#,
206 user_name,
207 operation,
208 operation,
209 admin_email,
210 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
211 );
212
213 self.send_email(user_email, &subject, &body_text).await
214 }
215
216 async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
218 let from = Mailbox::new(
219 Some(self.from_name.clone()),
220 self.from_email
221 .parse()
222 .map_err(|e| format!("Invalid from email: {}", e))?,
223 );
224
225 let to_mailbox = Mailbox::new(
226 None,
227 to.parse()
228 .map_err(|e| format!("Invalid recipient email: {}", e))?,
229 );
230
231 let email = Message::builder()
232 .from(from)
233 .to(to_mailbox)
234 .subject(subject)
235 .header(ContentType::TEXT_PLAIN)
236 .body(body.to_string())
237 .map_err(|e| format!("Failed to build email: {}", e))?;
238
239 let creds = Credentials::new(self.smtp_username.clone(), self.smtp_password.clone());
240
241 let mailer = SmtpTransport::relay(&self.smtp_host)
242 .map_err(|e| format!("Failed to create SMTP transport: {}", e))?
243 .port(self.smtp_port)
244 .credentials(creds)
245 .build();
246
247 let send_result = tokio::task::spawn_blocking(move || mailer.send(&email))
249 .await
250 .map_err(|e| format!("Failed to spawn blocking task: {}", e))?;
251
252 send_result.map_err(|e| format!("Failed to send email: {}", e))?;
253
254 log::info!("Email sent successfully to {}: {}", to, subject);
255 Ok(())
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_email_service_disabled() {
265 std::env::set_var("SMTP_ENABLED", "false");
266 let service = EmailService::from_env().unwrap();
267 assert!(!service.enabled);
268 }
269
270 #[tokio::test]
271 async fn test_send_export_notification_disabled() {
272 std::env::set_var("SMTP_ENABLED", "false");
273 let service = EmailService::from_env().unwrap();
274 let result = service
275 .send_gdpr_export_notification("test@example.com", "Test User", Uuid::new_v4())
276 .await;
277 assert!(result.is_ok());
278 }
279
280 #[tokio::test]
281 async fn test_send_erasure_notification_disabled() {
282 std::env::set_var("SMTP_ENABLED", "false");
283 let service = EmailService::from_env().unwrap();
284 let result = service
285 .send_gdpr_erasure_notification("test@example.com", "Test User", 2)
286 .await;
287 assert!(result.is_ok());
288 }
289}