Skip to main content

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}