Skip to main content

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