create_rust_app/
mailer.rs

1#[cfg(feature = "plugin_auth")]
2use crate::auth::mail::{
3    auth_activated, auth_password_changed, auth_password_reset, auth_recover_existent_account,
4    auth_recover_nonexistent_account, auth_register,
5};
6#[cfg(feature = "plugin_auth")]
7use dyn_clone::{clone_trait_object, DynClone};
8
9use lettre::message::{Message, MultiPart};
10use lettre::transport::smtp::authentication::Credentials;
11use lettre::transport::stub::StubTransport;
12use lettre::{SmtpTransport, Transport};
13
14// the DyncClone trait bound is for cloning, and the
15// Send trait bound is for thread-safety
16#[cfg(feature = "plugin_auth")]
17/// A trait that defines the behavior of an email template
18pub trait EmailTemplates: DynClone + Sync + Send {
19    fn send_activated(&self, mailer: &Mailer, to_email: &str);
20    fn send_password_changed(&self, mailer: &Mailer, to_email: &str);
21    fn send_password_reset(&self, mailer: &Mailer, to_email: &str);
22    fn send_recover_existent_account(&self, mailer: &Mailer, to_email: &str, link: &str);
23    fn send_recover_nonexistent_account(&self, mailer: &Mailer, to_email: &str, link: &str);
24    fn send_register(&self, mailer: &Mailer, to_email: &str, link: &str);
25}
26
27#[cfg(feature = "plugin_auth")]
28clone_trait_object!(EmailTemplates);
29
30#[derive(Clone)]
31/// struct used to handle sending emails
32pub struct Mailer {
33    /// the email address emails should be sent from
34    ///
35    /// set by the `SMTP_FROM_ADDRESS` environment variable
36    pub from_address: String,
37    /// the smtp server to connect to for purposes of sending emails
38    ///
39    /// set by the `SMTP_SERVER` environment variable
40    pub smtp_server: String,
41    /// username used to log into `SMTP_SERVER`
42    ///
43    /// set by the `SMTP_USERNAME` environment variable
44    pub smtp_username: String,
45    /// the password used to log into `SMTP_SERVER`
46    ///
47    /// set by the `SMTP_PASSWORD` environment variable
48    pub smtp_password: String,
49    /// whether or not emails should actually be sent when requested
50    ///
51    /// it may be useful to set this to false in some devolopment environments
52    /// while setting it to true in production
53    ///
54    /// set by the `SEND_MAIL` environment variable
55    pub actually_send: bool,
56    #[cfg(feature = "plugin_auth")]
57    // Structure containing email templates to be used for various purposes
58    pub templates: Box<dyn EmailTemplates + Sync + Send>,
59}
60
61impl Default for Mailer {
62    #[cfg(feature = "plugin_auth")]
63    fn default() -> Self {
64        Self::new(Box::<DefaultMailTemplates>::default())
65    }
66    #[cfg(not(feature = "plugin_auth"))]
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl Mailer {
73    /// using information stored in the `SMTP_FROM_ADDRESS`, `SMTP_SERVER`, `SMTP_USERNAME`, `SMTP_PASSWORD`, and `SEND_MAIL`
74    /// environment variables to connect to a remote SMTP server,
75    ///
76    /// allows webservers to send emails to users for purposes
77    /// like marketing, user authentification, etc.
78    #[cfg(not(feature = "plugin_auth"))]
79    pub fn new() -> Self {
80        Mailer::check_environment_variables();
81
82        let from_address: String = std::env::var("SMTP_FROM_ADDRESS")
83            .unwrap_or_else(|_| "create-rust-app@localhost".to_string());
84        let smtp_server: String = std::env::var("SMTP_SERVER").unwrap_or_else(|_| "".to_string());
85        let smtp_username: String =
86            std::env::var("SMTP_USERNAME").unwrap_or_else(|_| "".to_string());
87        let smtp_password: String =
88            std::env::var("SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
89        let actually_send: bool = std::env::var("SEND_MAIL")
90            .unwrap_or_else(|_| "false".to_string())
91            .eq_ignore_ascii_case("true");
92        Mailer {
93            from_address,
94            smtp_server,
95            smtp_username,
96            smtp_password,
97            actually_send,
98        }
99    }
100
101    #[cfg(feature = "plugin_auth")]
102    #[must_use]
103    pub fn new(templates: Box<dyn EmailTemplates + Sync + Send>) -> Self {
104        Self::check_environment_variables();
105
106        let from_address: String = std::env::var("SMTP_FROM_ADDRESS")
107            .unwrap_or_else(|_| "create-rust-app@localhost".to_string());
108        let smtp_server: String = std::env::var("SMTP_SERVER").unwrap_or_else(|_| String::new());
109        let smtp_username: String =
110            std::env::var("SMTP_USERNAME").unwrap_or_else(|_| String::new());
111        let smtp_password: String =
112            std::env::var("SMTP_PASSWORD").unwrap_or_else(|_| String::new());
113        let actually_send: bool = std::env::var("SEND_MAIL")
114            .unwrap_or_else(|_| "false".to_string())
115            .eq_ignore_ascii_case("true");
116        Self {
117            from_address,
118            smtp_server,
119            smtp_username,
120            smtp_password,
121            actually_send,
122            templates,
123        }
124    }
125
126    /// checks that the required environment variables are set
127    ///
128    /// prints messages denoting which, if any, of the required
129    /// environment variables were not set
130    ///
131    /// # Panics
132    ///
133    /// panics if any of the required environment variables aren't set properly
134    ///
135    /// TODO: it'd be better to return a Result and let the user handle the error
136    pub fn check_environment_variables() {
137        let vars = vec![
138            "SMTP_FROM_ADDRESS",
139            "SMTP_SERVER",
140            "SMTP_USERNAME",
141            "SMTP_PASSWORD",
142            "SEND_MAIL",
143        ];
144
145        let unset_vars = vars
146            .into_iter()
147            .filter(|v| std::env::var(v).is_err())
148            .collect::<Vec<_>>();
149
150        if !unset_vars.is_empty() {
151            println!(
152                "Warning: Mailing disabled; the following variables must be set: {}",
153                unset_vars.join(", ")
154            );
155        }
156
157        let send_mail_value = std::env::var("SEND_MAIL").unwrap_or_default();
158        if !send_mail_value.eq_ignore_ascii_case("true")
159            && !send_mail_value.eq_ignore_ascii_case("false")
160        {
161            println!("Warning: SEND_MAIL must be `true` or `false`");
162        }
163    }
164
165    /// send an email with the specifified content and subject to the specified user
166    ///
167    /// will only send an email if the `SEND_MAIL` environment variable was set to true when
168    /// this mailer was initialized.
169    ///
170    /// # Arguments
171    /// * `to` - a string slice that holds the email address of the intended recipient
172    /// * `subject` - subject field of the email
173    /// * `text` - text content of the email
174    /// * `html` - html content of the email
175    ///
176    /// # Panics
177    ///
178    /// panis if the `to` argument is not a valid email address
179    ///
180    /// TODO: wouldn't it be better to instead require the `to` argument be some wrapper around a string that is always a valid email address?
181    pub fn send(&self, to: &str, subject: &str, text: &str, html: &str) {
182        let email = Message::builder()
183            .to(to.parse().unwrap())
184            .from(self.from_address.parse().unwrap())
185            .subject(subject)
186            .multipart(MultiPart::alternative_plain_html(
187                String::from(text),
188                String::from(html),
189            ))
190            .unwrap();
191
192        if self.actually_send {
193            let mailer = SmtpTransport::relay(&self.smtp_server)
194                .unwrap()
195                .credentials(Credentials::new(
196                    self.smtp_username.to_string(),
197                    self.smtp_password.to_string(),
198                ))
199                .build();
200
201            let result = mailer.send(&email);
202            println!(
203                r#"====================
204Sent email {:#?}
205--------------------
206to: {:?}
207from: {}
208message:
209{}
210===================="#,
211                result, to, self.from_address, text
212            );
213        } else {
214            let mailer = StubTransport::new_ok();
215            let result = mailer.send(&email);
216            println!(
217                r#"====================
218Sent email {:#?}
219--------------------
220to: {:?}
221from: {}
222message:
223{}
224===================="#,
225                result, to, self.from_address, text
226            );
227        }
228    }
229}
230
231#[cfg(feature = "plugin_auth")]
232#[derive(Clone)]
233pub struct DefaultMailTemplates {
234    pub base_url: String,
235}
236#[cfg(feature = "plugin_auth")]
237impl DefaultMailTemplates {
238    #[must_use]
239    pub fn new(base_url: &str) -> Self {
240        Self {
241            base_url: base_url.to_string(),
242        }
243    }
244}
245#[cfg(feature = "plugin_auth")]
246impl Default for DefaultMailTemplates {
247    fn default() -> Self {
248        Self::new("http://localhost:3000/")
249    }
250}
251#[cfg(feature = "plugin_auth")]
252impl EmailTemplates for DefaultMailTemplates {
253    fn send_activated(&self, mailer: &Mailer, to_email: &str) {
254        auth_activated::send(mailer, to_email);
255    }
256    fn send_password_changed(&self, mailer: &Mailer, to_email: &str) {
257        auth_password_changed::send(mailer, to_email);
258    }
259    fn send_password_reset(&self, mailer: &Mailer, to_email: &str) {
260        auth_password_reset::send(mailer, to_email);
261    }
262    fn send_recover_existent_account(&self, mailer: &Mailer, to_email: &str, url_path: &str) {
263        auth_recover_existent_account::send(
264            mailer,
265            to_email,
266            format!("{base_url}{url_path}", base_url = self.base_url).as_str(),
267        );
268    }
269    fn send_recover_nonexistent_account(&self, mailer: &Mailer, to_email: &str, url_path: &str) {
270        auth_recover_nonexistent_account::send(
271            mailer,
272            to_email,
273            format!("{base_url}{url_path}", base_url = self.base_url).as_str(),
274        );
275    }
276    fn send_register(&self, mailer: &Mailer, to_email: &str, url_path: &str) {
277        auth_register::send(
278            mailer,
279            to_email,
280            format!("{base_url}{url_path}", base_url = self.base_url).as_str(),
281        );
282    }
283}