api/services/email.rs
1//! Email service module for handling email-related functionality.
2//!
3//! This module provides functionality for sending various types of emails using SMTP,
4//! specifically configured for Gmail. It uses the `lettre` crate for email handling
5//! and supports both plain text and HTML email formats.
6//!
7//! # Environment Variables Required
8//! - `GMAIL_USERNAME`: Gmail address to send emails from
9//! - `GMAIL_APP_PASSWORD`: Gmail app password for authentication
10//! - `FRONTEND_URL`: Base URL of the frontend application
11//! - `EMAIL_FROM_NAME`: Display name for the sender
12
13use lettre::{
14 message::{header, Message, MultiPart, SinglePart},
15 transport::smtp::{authentication::Credentials, AsyncSmtpTransport},
16 AsyncTransport, Tokio1Executor,
17};
18use lettre::transport::smtp::client::{Tls, TlsParameters};
19use once_cell::sync::Lazy;
20use std::env;
21
22/// Global SMTP client instance configured for Gmail.
23///
24/// This is initialized lazily when first used, using environment variables
25/// for configuration. The client is configured to use TLS and requires
26/// authentication.
27static SMTP_CLIENT: Lazy<AsyncSmtpTransport<Tokio1Executor>> = Lazy::new(|| {
28 let username = env::var("GMAIL_USERNAME").expect("GMAIL_USERNAME must be set");
29 let password = env::var("GMAIL_APP_PASSWORD").expect("GMAIL_APP_PASSWORD must be set");
30
31 let tls_parameters = TlsParameters::new("smtp.gmail.com".to_string())
32 .expect("Failed to create TLS parameters");
33
34 AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.gmail.com")
35 .expect("Failed to create SMTP transport")
36 .port(587)
37 .tls(Tls::Required(tls_parameters))
38 .credentials(Credentials::new(username, password))
39 .build()
40});
41
42/// Service for handling email-related operations.
43pub struct EmailService;
44
45impl EmailService {
46 /// Sends a password reset email to the specified email address.
47 ///
48 /// # Arguments
49 /// * `to_email` - The recipient's email address
50 /// * `reset_token` - The password reset token to include in the reset link
51 ///
52 /// # Returns
53 /// * `Result<(), Box<dyn std::error::Error>>` - Ok(()) if email was sent successfully,
54 /// Err containing the error if sending failed
55 ///
56 /// # Email Content
57 /// The email includes both plain text and HTML versions with:
58 /// * A personalized greeting
59 /// * A reset password link
60 /// * Expiration notice (15 minutes)
61 /// * Security warning for unintended recipients
62 /// * Styled HTML version with a clickable button
63 pub async fn send_password_reset_email(
64 to_email: &str,
65 reset_token: &str,
66 ) -> Result<(), Box<dyn std::error::Error>> {
67 let frontend_url = env::var("FRONTEND_URL").expect("FRONTEND_URL must be set");
68 let from_email = env::var("GMAIL_USERNAME").expect("GMAIL_USERNAME must be set");
69 let from_name = env::var("EMAIL_FROM_NAME").expect("EMAIL_FROM_NAME must be set");
70 let reset_link = format!("{}/reset-password?token={}", frontend_url, reset_token);
71
72 let email = Message::builder()
73 .from(format!("{} <{}>", from_name, from_email).parse().unwrap())
74 .to(to_email.parse().unwrap())
75 .subject("Reset Your Password")
76 .multipart(
77 MultiPart::alternative()
78 .singlepart(
79 SinglePart::builder()
80 .header(header::ContentType::TEXT_PLAIN)
81 .body(format!(
82 "Hello,\n\n\
83 You have requested to reset your password. Click the link below to proceed:\n\n\
84 {}\n\n\
85 This link will expire in 15 minutes.\n\n\
86 If you did not request this password reset, please ignore this email.\n\n\
87 Best regards,\n\
88 {}",
89 reset_link,
90 from_name
91 )),
92 )
93 .singlepart(
94 SinglePart::builder()
95 .header(header::ContentType::TEXT_HTML)
96 .body(format!(
97 r#"<!DOCTYPE html>
98 <html>
99 <head>
100 <style>
101 body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
102 .container {{ max-width: 600px; margin: 0 auto; padding: 20px; text-align: center; }}
103 .button {{
104 display: inline-block;
105 padding: 10px 20px;
106 background-color: #007bff;
107 color: #ffffff !important;
108 text-decoration: none;
109 border-radius: 5px;
110 margin: 20px 0;
111 font-weight: bold;
112 }}
113 .warning {{ color: #dc3545; }}
114 </style>
115 </head>
116 <body>
117 <div class="container">
118 <h2>Reset Your Password</h2>
119 <p>Hello,</p>
120 <p>You have requested to reset your password. Click the button below to proceed:</p>
121 <a href="{}" class="button">Reset Password</a>
122 <p>This link will expire in 15 minutes.</p>
123 <p class="warning">If you did not request this password reset, please ignore this email.</p>
124 <p>Best regards,<br>{}</p>
125 </div>
126 </body>
127 </html>"#,
128 reset_link, from_name
129 )),
130 ),
131 )?;
132
133 match SMTP_CLIENT.send(email).await {
134 Ok(_) => {
135 Ok(())
136 }
137 Err(e) => {
138 Err(Box::new(e) as Box<dyn std::error::Error>)
139 }
140 }
141 }
142
143 /// Sends a password change confirmation email to the specified email address.
144 ///
145 /// # Arguments
146 /// * `to_email` - The recipient's email address
147 ///
148 /// # Returns
149 /// * `Result<(), Box<dyn std::error::Error>>` - Ok(()) if email was sent successfully,
150 /// Err containing the error if sending failed
151 ///
152 /// # Email Content
153 /// The email includes both plain text and HTML versions with:
154 /// * Confirmation of password change
155 /// * Security warning for unintended changes
156 /// * Simple HTML formatting
157 pub async fn send_password_changed_email(
158 to_email: &str,
159 ) -> Result<(), Box<dyn std::error::Error>> {
160 let from_email = env::var("GMAIL_USERNAME").expect("GMAIL_USERNAME must be set");
161 let from_name = env::var("EMAIL_FROM_NAME").expect("EMAIL_FROM_NAME must be set");
162
163 let email = Message::builder()
164 .from(format!("{} <{}>", from_name, from_email).parse().unwrap())
165 .to(to_email.parse().unwrap())
166 .subject("Your Password Has Been Changed")
167 .multipart(
168 MultiPart::alternative()
169 .singlepart(
170 SinglePart::builder()
171 .header(header::ContentType::TEXT_PLAIN)
172 .body(format!(
173 "Hello,\n\n\
174 Your password has been successfully changed.\n\n\
175 If you did not make this change, please contact support immediately.\n\n\
176 Best regards,\n\
177 {}",
178 from_name
179 )),
180 )
181 .singlepart(
182 SinglePart::builder()
183 .header(header::ContentType::TEXT_HTML)
184 .body(format!(
185 "<html>\
186 <body>\
187 <p>Hello,</p>\
188 <p>Your password has been successfully changed.</p>\
189 <p>If you did not make this change, please contact support immediately.</p>\
190 <p>Best regards,<br>\
191 {}</p>\
192 </body>\
193 </html>",
194 from_name
195 )),
196 ),
197 )?;
198
199 match SMTP_CLIENT.send(email).await {
200 Ok(_) => {
201 Ok(())
202 }
203 Err(e) => {
204 Err(Box::new(e) as Box<dyn std::error::Error>)
205 }
206 }
207 }
208}