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}