async_mailer_smtp/lib.rs
1//! An SMTP mailer, usable either stand-alone or as either generic `Mailer` or dynamic `dyn DynMailer` using the `mail-send` crate.
2//!
3//! **Preferably, use [`async-mailer`](https://docs.rs/async-mailer), which re-exports from this crate,
4//! rather than using `async-mailer-smtp` directly.**
5//!
6//! You can control the re-exported mailer implementations,
7//! as well as [`tracing`](https://docs.rs/crate/tracing) support,
8//! via [`async-mailer` feature toggles](https://docs.rs/crate/async-mailer/latest/features).
9//!
10//! **Note:**
11//! If you are planning to always use `SmtpMailer` and do not need `async_mailer_outlook::OutlookMailer`
12//! or `async_mailer::BoxMailer`, then consider using the [`mail-send`](https://docs.rs/mail-send) crate directly.
13//!
14//! # Examples
15//!
16//! ## Using the statically typed `Mailer`:
17//!
18//! ```no_run
19//! # async fn test() -> Result<(), Box<dyn std::error::Error>> {
20//! // Both `async_mailer::OutlookMailer` and `async_mailer::SmtpMailer` implement `Mailer`
21//! // and can be used with `impl Mailer` or `<M: Mailer>` bounds.
22//!
23//! # use async_mailer_smtp::{SmtpMailer, SmtpInvalidCertsPolicy};
24//! let mailer = SmtpMailer::new(
25//! "smtp.example.com".into(),
26//! 465,
27//! SmtpInvalidCertsPolicy::Deny,
28//! "<username>".into(),
29//! secrecy::SecretString::from("<password>")
30//! )?;
31//!
32//! // An alternative `OutlookMailer` can be found at `async-mailer-outlook`.
33//! // Further alternative mailers can be implemented by third parties.
34//!
35//! // Build a message using the re-exported `mail_builder::MessageBuilder'.
36//! //
37//! // For blazingly fast rendering of beautiful HTML mail,
38//! // I recommend combining `askama` with `mrml`.
39//!
40//! # use async_mailer_core::mail_send::smtp::message::IntoMessage;
41//! let message = async_mailer_core::mail_send::mail_builder::MessageBuilder::new()
42//! .from(("From Name", "from@example.com"))
43//! .to("to@example.com")
44//! .subject("Subject")
45//! .text_body("Mail body")
46//! .into_message()?;
47//!
48//! // Send the message using the statically typed `Mailer`.
49//!
50//! # use async_mailer_core::Mailer;
51//! mailer.send_mail(message).await?;
52//! # Ok(())
53//! # }
54//! ```
55//!
56//! ## Using the dynamically typed `DynMailer`:
57//!
58//! ```no_run
59//! # async fn test() -> Result<(), async_mailer_core::DynMailerError> {
60//! // Both `async_mailer::OutlookMailer` and `async_mailer::SmtpMailer`
61//! // implement `DynMailer` and can be used as trait objects.
62//! //
63//! // Here they are used as `BoxMailer`, which is an alias to `Box<dyn DynMailer>`.
64//!
65//! # use async_mailer_core::BoxMailer;
66//! # use async_mailer_smtp::{SmtpMailer, SmtpInvalidCertsPolicy};
67//! let mailer: BoxMailer = SmtpMailer::new_box( // Or `SmtpMailer::new_arc()`.
68//! "smtp.example.com".into(),
69//! 465,
70//! SmtpInvalidCertsPolicy::Deny,
71//! "<username>".into(),
72//! secrecy::SecretString::from("<password>")
73//! )?;
74//!
75//! // An alternative `OutlookMailer` can be found at `async-mailer-outlook`.
76//! // Further alternative mailers can be implemented by third parties.
77//!
78//! // The trait object is `Send` and `Sync` and may be stored e.g. as part of your server state.
79//!
80//! // Build a message using the re-exported `mail_builder::MessageBuilder'.
81//! //
82//! // For blazingly fast rendering of beautiful HTML mail,
83//! // I recommend combining `askama` with `mrml`.
84//!
85//! # use async_mailer_core::mail_send::smtp::message::IntoMessage;
86//! let message = async_mailer_core::mail_send::mail_builder::MessageBuilder::new()
87//! .from(("From Name", "from@example.com"))
88//! .to("to@example.com")
89//! .subject("Subject")
90//! .text_body("Mail body")
91//! .into_message()?;
92//!
93//! // Send the message using the implementation-agnostic `dyn DynMailer`.
94//!
95//! mailer.send_mail(message).await?;
96//! # Ok(())
97//! # }
98//! ```
99//!
100//! # Feature flags
101//!
102//! - `tracing`: Enable debug and error logging using the [`tracing`](https://docs.rs/crate/tracing) crate.
103//! All relevant functions are instrumented.
104//! - `clap`: Implement [`clap::ValueEnum`](https://docs.rs/clap/latest/clap/trait.ValueEnum.html) for [`SmtpInvalidCertsPolicy`].
105//! This allows for easily configured CLI options like `--invalid-certs <allow|deny>`.
106//!
107//! Default: `tracing`.
108//!
109//! ## Roadmap
110//!
111//! DKIM support is planned to be implemented on the [`SmtpMailer`].
112
113use std::sync::Arc;
114use std::time::Duration;
115
116use async_trait::async_trait;
117
118#[cfg(feature = "clap")]
119use clap;
120
121use secrecy::{ExposeSecret, SecretString};
122
123#[cfg(feature = "tracing")]
124use tracing::{error, info, instrument};
125
126use async_mailer_core::mail_send::{self, smtp::message::Message, SmtpClientBuilder};
127use async_mailer_core::{util, ArcMailer, BoxMailer, DynMailer, DynMailerError, Mailer};
128
129/// Error returned by [`SmtpMailer::new`] and [`SmtpMailer::send_mail`].
130#[derive(Debug, thiserror::Error)]
131pub enum SmtpMailerError {
132 /// Failed to build the SMTP client.
133 #[error("failed to build SMTP client: {0}")]
134 Build(String),
135
136 /// Could not connect to SMTP host.
137 #[error("could not connect to SMTP host: {0}")]
138 Connect(#[source] mail_send::Error),
139
140 /// Could not send SMTP mail.
141 #[error("could not send SMTP mail: {0}")]
142 Send(#[source] mail_send::Error),
143}
144
145/// Pass to [`SmtpMailer::new`] to either allow or deny invalid SMTP certificates.
146///
147/// This option allows to perform tests or local development work against
148/// SMTP development servers like MailHog or MailPit, while using a self-signed certificate.
149///
150/// **Never use [`SmtpInvalidCertsPolicy::Allow`] in production!**
151// TODO: derive Clap ValueEnum
152#[derive(Clone, Debug, Default)]
153#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
154pub enum SmtpInvalidCertsPolicy {
155 /// Allow connecting to SMTP servers with invalid TLS certificates.
156 ///
157 /// **Do not use in production!**
158 Allow,
159
160 /// Deny connecting to SMTP servers with invalid TLS certificates.
161 ///
162 /// This variant is the [`Default`].
163 #[default]
164 Deny,
165}
166
167/// An SMTP mailer client, implementing the [`async_mailer_core::Mailer`](https://docs.rs/async-mailer/latest/async_mailer/trait.Mailer.html)
168/// and [`async_mailer_core::DynMailer`](https://docs.rs/async-mailer/latest/async_mailer/trait.DynMailer.html) traits
169/// to be used as generic mailer or runtime-pluggable trait object.
170///
171/// An abstraction over [`mail-send`](https://docs.rs/mail-send), sending mail via an SMTP connection.
172///
173/// Self-signed certificates can optionally be accepted, to use the SMTP mailer in development while using the Outlook mailer in production.
174#[derive(Clone)]
175pub struct SmtpMailer {
176 inner: SmtpClientBuilder<String>,
177}
178
179impl std::fmt::Debug for SmtpMailer {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 f.debug_struct("Client (SMTP)").finish()
182 }
183}
184
185impl SmtpMailer {
186 /// Create a new SMTP mailer client.
187 ///
188 /// # Errors
189 ///
190 /// Returns a [`SmtpMailerError::Build`] error
191 /// if the SMTP client cannot be built.
192 #[cfg_attr(feature = "tracing", instrument)]
193 pub fn new(
194 host: String,
195 port: u16,
196 invalid_certs: SmtpInvalidCertsPolicy,
197 user: String,
198 password: SecretString,
199 ) -> Result<Self, SmtpMailerError> {
200 let mut smtp_client = SmtpClientBuilder::new(host, port)
201 .map_err(SmtpMailerError::Build)?
202 .credentials((user, password.expose_secret().into()))
203 .timeout(Duration::from_secs(30));
204
205 if matches!(invalid_certs, SmtpInvalidCertsPolicy::Allow) {
206 smtp_client = smtp_client.allow_invalid_certs();
207 }
208
209 Ok(Self { inner: smtp_client })
210 }
211
212 /// Create a new SMTP mailer client as dynamic `async_mailer::BoxMailer`.
213 ///
214 /// # Errors
215 ///
216 /// Returns a [`SmtpMailerError::Build`] error
217 /// if the SMTP client cannot be built.
218 #[cfg_attr(feature = "tracing", instrument)]
219 pub fn new_box(
220 host: String,
221 port: u16,
222 invalid_certs: SmtpInvalidCertsPolicy,
223 user: String,
224 password: SecretString,
225 ) -> Result<BoxMailer, SmtpMailerError> {
226 Ok(Box::new(Self::new(
227 host,
228 port,
229 invalid_certs,
230 user,
231 password,
232 )?))
233 }
234
235 /// Create a new SMTP mailer client as dynamic `async_mailer::ArcMailer`.
236 ///
237 /// # Errors
238 ///
239 /// Returns a [`SmtpMailerError::Build`] error
240 /// if the SMTP client cannot be built.
241 #[cfg_attr(feature = "tracing", instrument)]
242 pub fn new_arc(
243 host: String,
244 port: u16,
245 invalid_certs: SmtpInvalidCertsPolicy,
246 user: String,
247 password: SecretString,
248 ) -> Result<ArcMailer, SmtpMailerError> {
249 Ok(Arc::new(Self::new(
250 host,
251 port,
252 invalid_certs,
253 user,
254 password,
255 )?))
256 }
257}
258
259// == Mailer ==
260
261#[async_trait]
262impl Mailer for SmtpMailer {
263 type Error = SmtpMailerError;
264
265 /// Send the prepared MIME message via an SMTP connection, using the previously configured credentials.
266 ///
267 /// # Errors
268 ///
269 /// Returns an [`SmtpMailerError::Connect`] error if a connection to the SMTP server cannot be established.
270 ///
271 /// Returns an [`SmtpMailerError::Send`] error if the connection was established but sending the e-mail message failed.
272 async fn send_mail(&self, message: Message<'_>) -> Result<(), Self::Error> {
273 #[cfg(feature = "tracing")]
274 // Extract recipient addresses for tracing log output.
275 let recipient_addresses = util::format_recipient_addresses(&message);
276
277 info!("Sending SMTP mail to {recipient_addresses}...");
278
279 let connection = self.inner.connect().await;
280
281 #[cfg(feature = "tracing")]
282 match &connection {
283 Ok(_) => {}
284 Err(error) => error!(
285 ?error,
286 "Failed to connect to SMTP host for mail to {recipient_addresses}"
287 ),
288 }
289
290 let response = connection
291 .map_err(SmtpMailerError::Connect)?
292 .send(message)
293 .await;
294
295 #[cfg(feature = "tracing")]
296 match &response {
297 Ok(_) => {
298 info!("Sent SMTP mail to {recipient_addresses}");
299 }
300 Err(error) => {
301 error!(?error, "Failed to send SMTP mail to {recipient_addresses}");
302 }
303 }
304
305 Ok(response.map_err(SmtpMailerError::Send)?)
306 }
307}
308
309// == DynMailer ==
310
311#[async_trait]
312impl DynMailer for SmtpMailer {
313 /// Send the prepared MIME message via an SMTP connection, using the previously configured credentials.
314 ///
315 /// # Errors
316 ///
317 /// Returns a boxed, type-erased [`SmtpMailerError::Connect`] error if a connection to the SMTP server cannot be established.
318 ///
319 /// Returns a boxed, type-erased [`SmtpMailerError::Send`] error if the connection was established but sending the e-mail message failed.
320 #[cfg_attr(feature = "tracing", instrument(skip(message)))]
321 async fn send_mail(&self, message: Message<'_>) -> Result<(), DynMailerError> {
322 Mailer::send_mail(self, message).await.map_err(Into::into)
323 }
324}