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    /// Could not connect to SMTP host.
133    #[error("could not connect to SMTP host")]
134    Connect(mail_send::Error),
135
136    /// Could not send SMTP mail.
137    #[error("could not send SMTP mail")]
138    Send(mail_send::Error),
139}
140
141/// Pass to [`SmtpMailer::new`] to either allow or deny invalid SMTP certificates.
142///
143/// This option allows to perform tests or local development work against
144/// SMTP development servers like MailHog or MailPit, while using a self-signed certificate.
145///
146/// **Never use [`SmtpInvalidCertsPolicy::Allow`] in production!**
147// TODO: derive Clap ValueEnum
148#[derive(Clone, Debug, Default)]
149#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
150pub enum SmtpInvalidCertsPolicy {
151    /// Allow connecting to SMTP servers with invalid TLS certificates.
152    ///
153    /// **Do not use in production!**
154    Allow,
155
156    /// Deny connecting to SMTP servers with invalid TLS certificates.
157    ///
158    /// This variant is the [`Default`].
159    #[default]
160    Deny,
161}
162
163/// An SMTP mailer client, implementing the [`async_mailer_core::Mailer`](https://docs.rs/async-mailer/latest/async_mailer/trait.Mailer.html)
164/// and [`async_mailer_core::DynMailer`](https://docs.rs/async-mailer/latest/async_mailer/trait.DynMailer.html) traits
165/// to be used as generic mailer or runtime-pluggable trait object.
166///
167/// An abstraction over [`mail-send`](https://docs.rs/mail-send), sending mail via an SMTP connection.
168///
169/// Self-signed certificates can optionally be accepted, to use the SMTP mailer in development while using the Outlook mailer in production.
170#[derive(Clone)]
171pub struct SmtpMailer {
172    inner: SmtpClientBuilder<String>,
173}
174
175impl std::fmt::Debug for SmtpMailer {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        f.debug_struct("Client (SMTP)").finish()
178    }
179}
180
181impl SmtpMailer {
182    /// Create a new SMTP mailer client.
183    #[cfg_attr(feature = "tracing", instrument)]
184    pub fn new(
185        host: String,
186        port: u16,
187        invalid_certs: SmtpInvalidCertsPolicy,
188        user: String,
189        password: SecretString,
190    ) -> Self {
191        let mut smtp_client = SmtpClientBuilder::new(host, port)
192            .credentials((user, password.expose_secret().into()))
193            .timeout(Duration::from_secs(30));
194
195        if matches!(invalid_certs, SmtpInvalidCertsPolicy::Allow) {
196            smtp_client = smtp_client.allow_invalid_certs();
197        }
198
199        Self { inner: smtp_client }
200    }
201
202    /// Create a new SMTP mailer client as dynamic `async_mailer::BoxMailer`.
203    #[cfg_attr(feature = "tracing", instrument)]
204    pub fn new_box(
205        host: String,
206        port: u16,
207        invalid_certs: SmtpInvalidCertsPolicy,
208        user: String,
209        password: SecretString,
210    ) -> BoxMailer {
211        Box::new(Self::new(host, port, invalid_certs, user, password))
212    }
213
214    /// Create a new SMTP mailer client as dynamic `async_mailer::ArcMailer`.
215    #[cfg_attr(feature = "tracing", instrument)]
216    pub fn new_arc(
217        host: String,
218        port: u16,
219        invalid_certs: SmtpInvalidCertsPolicy,
220        user: String,
221        password: SecretString,
222    ) -> ArcMailer {
223        Arc::new(Self::new(host, port, invalid_certs, user, password))
224    }
225}
226
227// == Mailer ==
228
229#[async_trait]
230impl Mailer for SmtpMailer {
231    type Error = SmtpMailerError;
232
233    /// Send the prepared MIME message via an SMTP connection, using the previously configured credentials.
234    ///
235    /// # Errors
236    ///
237    /// Returns an [`SmtpMailerError::Connect`] error if a connection to the SMTP server cannot be established.
238    ///
239    /// Returns an [`SmtpMailerError::Send`] error if the connection was established but sending the e-mail message failed.
240    async fn send_mail(&self, message: Message<'_>) -> Result<(), Self::Error> {
241        #[cfg(feature = "tracing")]
242        // Extract recipient addresses for tracing log output.
243        let recipient_addresses = util::format_recipient_addresses(&message);
244
245        info!("Sending SMTP mail to {recipient_addresses}...");
246
247        let connection = self.inner.connect().await;
248
249        #[cfg(feature = "tracing")]
250        match &connection {
251            Ok(_) => {}
252            Err(error) => error!(
253                ?error,
254                "Failed to connect to SMTP host for mail to {recipient_addresses}"
255            ),
256        }
257
258        let response = connection
259            .map_err(SmtpMailerError::Connect)?
260            .send(message)
261            .await;
262
263        #[cfg(feature = "tracing")]
264        match &response {
265            Ok(_) => {
266                info!("Sent SMTP mail to {recipient_addresses}");
267            }
268            Err(error) => {
269                error!(?error, "Failed to send SMTP mail to {recipient_addresses}");
270            }
271        }
272
273        Ok(response.map_err(SmtpMailerError::Send)?)
274    }
275}
276
277// == DynMailer ==
278
279#[async_trait]
280impl DynMailer for SmtpMailer {
281    /// Send the prepared MIME message via an SMTP connection, using the previously configured credentials.
282    ///
283    /// # Errors
284    ///
285    /// Returns a boxed, type-erased [`SmtpMailerError::Connect`] error if a connection to the SMTP server cannot be established.
286    ///
287    /// Returns a boxed, type-erased [`SmtpMailerError::Send`] error if the connection was established but sending the e-mail message failed.
288    #[cfg_attr(feature = "tracing", instrument(skip(message)))]
289    async fn send_mail(&self, message: Message<'_>) -> Result<(), DynMailerError> {
290        Mailer::send_mail(self, message).await.map_err(Into::into)
291    }
292}