email/smtp/
config.rs

1//! Module dedicated to the SMTP sender configuration.
2//!
3//! This module contains the configuration specific to the SMTP
4//! sender.
5
6use std::io;
7
8use mail_send::Credentials;
9use tracing::debug;
10
11#[doc(inline)]
12pub use super::{Error, Result};
13#[cfg(feature = "oauth2")]
14use crate::account::config::oauth2::{OAuth2Config, OAuth2Method};
15use crate::{account::config::passwd::PasswordConfig, tls::Encryption};
16
17/// The SMTP sender configuration.
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
19#[cfg_attr(
20    feature = "derive",
21    derive(serde::Serialize, serde::Deserialize),
22    serde(rename_all = "kebab-case")
23)]
24pub struct SmtpConfig {
25    /// The SMTP server host name.
26    pub host: String,
27
28    /// The SMTP server host port.
29    pub port: u16,
30
31    /// The SMTP encryption protocol to use.
32    ///
33    /// Supported encryption: SSL/TLS or STARTTLS.
34    pub encryption: Option<Encryption>,
35
36    /// The SMTP server login.
37    ///
38    /// Usually, the login is either the email address or its left
39    /// part (before @).
40    pub login: String,
41
42    /// The SMTP server authentication configuration.
43    ///
44    /// Authentication can be done using password or OAuth 2.0.
45    /// See [SmtpAuthConfig].
46    pub auth: SmtpAuthConfig,
47}
48
49impl SmtpConfig {
50    /// Return `true` if TLS or StartTLS is enabled.
51    pub fn is_encryption_enabled(&self) -> bool {
52        matches!(
53            self.encryption.as_ref(),
54            None | Some(Encryption::Tls(_)) | Some(Encryption::StartTls(_))
55        )
56    }
57
58    /// Return `true` if StartTLS is enabled.
59    pub fn is_start_tls_encryption_enabled(&self) -> bool {
60        matches!(self.encryption.as_ref(), Some(Encryption::StartTls(_)))
61    }
62
63    /// Return `true` if encryption is disabled.
64    pub fn is_encryption_disabled(&self) -> bool {
65        matches!(self.encryption.as_ref(), Some(Encryption::None))
66    }
67
68    /// Builds the SMTP credentials string.
69    ///
70    /// The result depends on the [`SmtpAuthConfig`]: if password mode
71    /// then creates credentials from login/password, if OAuth 2.0
72    /// then creates credentials from access token.
73    pub async fn credentials(&self) -> Result<Credentials<String>> {
74        Ok(match &self.auth {
75            SmtpAuthConfig::Password(passwd) => {
76                let passwd = passwd.get().await.map_err(Error::GetPasswdSmtpError)?;
77                let passwd = passwd
78                    .lines()
79                    .next()
80                    .ok_or(Error::GetPasswdEmptySmtpError)?;
81                Credentials::new(self.login.clone(), passwd.to_owned())
82            }
83            #[cfg(feature = "oauth2")]
84            SmtpAuthConfig::OAuth2(oauth2) => {
85                let access_token = oauth2
86                    .access_token()
87                    .await
88                    .map_err(|_| Error::AccessTokenWasNotAvailable)?;
89
90                match oauth2.method {
91                    OAuth2Method::XOAuth2 => {
92                        Credentials::new_xoauth2(self.login.clone(), access_token)
93                    }
94                    OAuth2Method::OAuthBearer => Credentials::new_oauth(access_token),
95                }
96            }
97        })
98    }
99}
100
101/// The SMTP authentication configuration.
102#[derive(Clone, Debug, Eq, PartialEq)]
103#[cfg_attr(
104    feature = "derive",
105    derive(serde::Serialize, serde::Deserialize),
106    serde(rename_all = "lowercase"),
107    serde(tag = "type"),
108    serde(from = "SmtpAuthConfigDerive")
109)]
110pub enum SmtpAuthConfig {
111    /// The password authentication mechanism.
112    Password(PasswordConfig),
113
114    /// The OAuth 2.0 authentication mechanism.
115    #[cfg(feature = "oauth2")]
116    OAuth2(OAuth2Config),
117}
118
119impl SmtpAuthConfig {
120    /// Resets the OAuth 2.0 authentication tokens.
121    pub async fn reset(&mut self) -> Result<()> {
122        debug!("resetting smtp backend configuration");
123
124        #[cfg(feature = "oauth2")]
125        if let Self::OAuth2(oauth2) = self {
126            oauth2
127                .reset()
128                .await
129                .map_err(|_| Error::ResettingOAuthFailed)?;
130        }
131
132        Ok(())
133    }
134
135    /// Configures the OAuth 2.0 authentication tokens.
136    pub async fn configure(
137        &mut self,
138        #[cfg_attr(not(feature = "oauth2"), allow(unused_variables))]
139        get_client_secret: impl Fn() -> io::Result<String>,
140    ) -> Result<()> {
141        debug!("configuring smtp backend");
142
143        #[cfg(feature = "oauth2")]
144        if let Self::OAuth2(oauth2) = self {
145            oauth2
146                .configure(get_client_secret)
147                .await
148                .map_err(|_| Error::ConfiguringOAuthFailed)?;
149        }
150
151        Ok(())
152    }
153
154    #[cfg(feature = "keyring")]
155    pub fn replace_empty_secrets(&mut self, name: impl AsRef<str>) -> Result<()> {
156        let name = name.as_ref();
157
158        match self {
159            SmtpAuthConfig::Password(secret) => {
160                secret
161                    .replace_with_keyring_if_empty(format!("{name}-smtp-passwd"))
162                    .map_err(Error::ReplacingKeyringFailed)?;
163            }
164            #[cfg(feature = "oauth2")]
165            SmtpAuthConfig::OAuth2(config) => {
166                if let Some(secret) = config.client_secret.as_mut() {
167                    secret
168                        .replace_with_keyring_if_empty(format!("{name}-smtp-oauth2-client-secret"))
169                        .map_err(Error::ReplacingKeyringFailed)?;
170                }
171
172                config
173                    .access_token
174                    .replace_with_keyring_if_empty(format!("{name}-smtp-oauth2-access-token"))
175                    .map_err(Error::ReplacingKeyringFailed)?;
176                config
177                    .refresh_token
178                    .replace_with_keyring_if_empty(format!("{name}-smtp-oauth2-refresh-token"))
179                    .map_err(Error::ReplacingKeyringFailed)?;
180            }
181        }
182
183        Ok(())
184    }
185}
186
187impl Default for SmtpAuthConfig {
188    fn default() -> Self {
189        Self::Password(PasswordConfig::default())
190    }
191}
192
193#[cfg(feature = "derive")]
194#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
195#[serde(rename_all = "lowercase", tag = "type")]
196pub enum SmtpAuthConfigDerive {
197    Password(PasswordConfig),
198    #[cfg(feature = "oauth2")]
199    OAuth2(OAuth2Config),
200    #[cfg(not(feature = "oauth2"))]
201    #[serde(skip_serializing, deserialize_with = "missing_oauth2_feature")]
202    OAuth2,
203}
204
205#[cfg(all(feature = "derive", not(feature = "oauth2")))]
206fn missing_oauth2_feature<'de, D>(_: D) -> std::result::Result<(), D::Error>
207where
208    D: serde::Deserializer<'de>,
209{
210    Err(serde::de::Error::custom("missing `oauth2` cargo feature"))
211}
212
213#[cfg(feature = "derive")]
214impl From<SmtpAuthConfigDerive> for SmtpAuthConfig {
215    fn from(config: SmtpAuthConfigDerive) -> Self {
216        match config {
217            SmtpAuthConfigDerive::Password(config) => Self::Password(config),
218            #[cfg(feature = "oauth2")]
219            SmtpAuthConfigDerive::OAuth2(config) => Self::OAuth2(config),
220            #[cfg(not(feature = "oauth2"))]
221            SmtpAuthConfigDerive::OAuth2 => unreachable!(),
222        }
223    }
224}