email/imap/
config.rs

1//! Module dedicated to the IMAP backend configuration.
2//!
3//! This module contains the implementation of the IMAP backend and
4//! all associated structures related to it.
5
6#[doc(inline)]
7use super::{Error, Result};
8#[cfg(feature = "oauth2")]
9use crate::account::config::oauth2::OAuth2Config;
10use crate::{account::config::passwd::PasswordConfig, tls::Encryption};
11
12/// Errors related to the IMAP backend configuration.
13
14/// The IMAP backend configuration.
15#[derive(Clone, Debug, Default, Eq, PartialEq)]
16#[cfg_attr(
17    feature = "derive",
18    derive(serde::Serialize, serde::Deserialize),
19    serde(rename_all = "kebab-case")
20)]
21pub struct ImapConfig {
22    /// The IMAP server host name.
23    pub host: String,
24
25    /// The IMAP server host port.
26    pub port: u16,
27
28    /// The IMAP encryption protocol to use.
29    ///
30    /// Supported encryption: SSL/TLS, STARTTLS or none.
31    pub encryption: Option<Encryption>,
32
33    /// The IMAP server login.
34    ///
35    /// Usually, the login is either the email address or its left
36    /// part (before @).
37    pub login: String,
38
39    /// The IMAP server authentication configuration.
40    ///
41    /// Authentication can be done using password or OAuth 2.0.
42    /// See [ImapAuthConfig].
43    pub auth: ImapAuthConfig,
44
45    /// The IMAP extensions configuration.
46    pub extensions: Option<ImapExtensionsConfig>,
47
48    /// The IMAP notify command.
49    ///
50    /// Defines the command used to notify the user when a new email is available.
51    /// Defaults to `notify-send "📫 <sender>" "<subject>"`.
52    pub watch: Option<ImapWatchConfig>,
53
54    /// The IMAP clients pool size.
55    ///
56    /// Defines the number of clients that are created and managed
57    /// simultaneously by the IMAP context. Defaults to 1.
58    pub clients_pool_size: Option<u8>,
59}
60
61impl ImapConfig {
62    pub fn clients_pool_size(&self) -> u8 {
63        self.clients_pool_size.unwrap_or(1)
64    }
65
66    pub fn send_id_after_auth(&self) -> bool {
67        self.extensions
68            .as_ref()
69            .and_then(|ext| ext.id.as_ref())
70            .and_then(|id| id.send_after_auth)
71            .unwrap_or_default()
72    }
73
74    /// Return `true` if TLS or StartTLS is enabled.
75    pub fn is_encryption_enabled(&self) -> bool {
76        matches!(
77            self.encryption.as_ref(),
78            None | Some(Encryption::Tls(_)) | Some(Encryption::StartTls(_))
79        )
80    }
81
82    /// Return `true` if StartTLS is enabled.
83    pub fn is_start_tls_encryption_enabled(&self) -> bool {
84        matches!(self.encryption.as_ref(), Some(Encryption::StartTls(_)))
85    }
86
87    /// Return `true` if encryption is disabled.
88    pub fn is_encryption_disabled(&self) -> bool {
89        matches!(self.encryption.as_ref(), Some(Encryption::None))
90    }
91
92    /// Builds authentication credentials.
93    ///
94    /// Authentication credentials can be either a password or an
95    /// OAuth 2.0 access token.
96    pub async fn build_credentials(&self) -> Result<String> {
97        self.auth.build_credentials().await
98    }
99
100    /// Find the IMAP watch timeout.
101    pub fn find_watch_timeout(&self) -> Option<u64> {
102        self.watch.as_ref().and_then(|c| c.find_timeout())
103    }
104}
105
106#[cfg(feature = "sync")]
107impl crate::sync::hash::SyncHash for ImapConfig {
108    fn sync_hash(&self, state: &mut std::hash::DefaultHasher) {
109        use std::hash::Hash;
110
111        Hash::hash(&self.host, state);
112        Hash::hash(&self.port, state);
113        Hash::hash(&self.login, state);
114    }
115}
116
117/// The IMAP authentication configuration.
118///
119/// Authentication can be done using password or OAuth 2.0.
120#[derive(Clone, Debug, Eq, PartialEq)]
121#[cfg_attr(
122    feature = "derive",
123    derive(serde::Serialize, serde::Deserialize),
124    serde(rename_all = "lowercase"),
125    serde(tag = "type"),
126    serde(from = "ImapAuthConfigDerive")
127)]
128pub enum ImapAuthConfig {
129    /// The password configuration.
130    Password(PasswordConfig),
131    /// The OAuth 2.0 configuration.
132    #[cfg(feature = "oauth2")]
133    OAuth2(OAuth2Config),
134}
135
136impl ImapAuthConfig {
137    /// Reset IMAP secrets (password or OAuth 2.0 tokens).
138    pub async fn reset(&self) -> Result<()> {
139        match self {
140            ImapAuthConfig::Password(config) => {
141                config.reset().await.map_err(Error::ResetPasswordError)
142            }
143            #[cfg(feature = "oauth2")]
144            ImapAuthConfig::OAuth2(config) => {
145                config.reset().await.map_err(Error::ResetOAuthSecretsError)
146            }
147        }
148    }
149
150    /// Builds authentication credentials.
151    ///
152    /// Authentication credentials can be either a password or an
153    /// OAuth 2.0 access token.
154    pub async fn build_credentials(&self) -> Result<String> {
155        match self {
156            ImapAuthConfig::Password(passwd) => {
157                let passwd = passwd.get().await.map_err(Error::GetPasswdImapError)?;
158                let passwd = passwd
159                    .lines()
160                    .next()
161                    .ok_or(Error::GetPasswdEmptyImapError)?;
162                Ok(passwd.to_owned())
163            }
164            #[cfg(feature = "oauth2")]
165            ImapAuthConfig::OAuth2(oauth2) => Ok(oauth2
166                .access_token()
167                .await
168                .map_err(Error::AccessTokenNotAvailable)?),
169        }
170    }
171
172    #[cfg(feature = "keyring")]
173    pub fn replace_empty_secrets(&mut self, name: impl AsRef<str>) -> Result<()> {
174        let name = name.as_ref();
175
176        match self {
177            Self::Password(secret) => {
178                secret
179                    .replace_with_keyring_if_empty(format!("{name}-imap-passwd"))
180                    .map_err(Error::ReplacingUnidentifiedFailed)?;
181            }
182            #[cfg(feature = "oauth2")]
183            Self::OAuth2(config) => {
184                if let Some(secret) = config.client_secret.as_mut() {
185                    secret
186                        .replace_with_keyring_if_empty(format!("{name}-imap-oauth2-client-secret"))
187                        .map_err(Error::ReplacingUnidentifiedFailed)?;
188                }
189
190                config
191                    .access_token
192                    .replace_with_keyring_if_empty(format!("{name}-imap-oauth2-access-token"))
193                    .map_err(Error::ReplacingUnidentifiedFailed)?;
194                config
195                    .refresh_token
196                    .replace_with_keyring_if_empty(format!("{name}-imap-oauth2-refresh-token"))
197                    .map_err(Error::ReplacingUnidentifiedFailed)?;
198            }
199        }
200
201        Ok(())
202    }
203}
204
205impl Default for ImapAuthConfig {
206    fn default() -> Self {
207        Self::Password(Default::default())
208    }
209}
210
211#[cfg(feature = "derive")]
212#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
213#[serde(rename_all = "lowercase", tag = "type")]
214pub enum ImapAuthConfigDerive {
215    Password(PasswordConfig),
216    #[cfg(feature = "oauth2")]
217    OAuth2(OAuth2Config),
218    #[cfg(not(feature = "oauth2"))]
219    #[serde(skip_serializing, deserialize_with = "missing_oauth2_feature")]
220    OAuth2,
221}
222
223#[cfg(all(feature = "derive", not(feature = "oauth2")))]
224fn missing_oauth2_feature<'de, D>(_: D) -> std::result::Result<(), D::Error>
225where
226    D: serde::Deserializer<'de>,
227{
228    Err(serde::de::Error::custom("missing `oauth2` cargo feature"))
229}
230
231#[cfg(feature = "derive")]
232impl From<ImapAuthConfigDerive> for ImapAuthConfig {
233    fn from(config: ImapAuthConfigDerive) -> Self {
234        match config {
235            ImapAuthConfigDerive::Password(config) => Self::Password(config),
236            #[cfg(feature = "oauth2")]
237            ImapAuthConfigDerive::OAuth2(config) => Self::OAuth2(config),
238            #[cfg(not(feature = "oauth2"))]
239            ImapAuthConfigDerive::OAuth2 => unreachable!(),
240        }
241    }
242}
243
244/// The IMAP watch options (IDLE).
245///
246/// Options dedicated to the IMAP IDLE mode, which is used to watch
247/// changes.
248#[derive(Clone, Debug, Eq, PartialEq)]
249#[cfg_attr(
250    feature = "derive",
251    derive(serde::Serialize, serde::Deserialize),
252    serde(rename_all = "kebab-case")
253)]
254pub struct ImapWatchConfig {
255    /// The IMAP watch timeout.
256    ///
257    /// Timeout used to refresh the IDLE command in
258    /// background. Defaults to 29 min as defined in the RFC.
259    timeout: Option<u64>,
260}
261
262impl ImapWatchConfig {
263    /// Find the IMAP watch timeout.
264    pub fn find_timeout(&self) -> Option<u64> {
265        self.timeout
266    }
267}
268
269/// The IMAP configuration dedicated to extensions.
270#[derive(Clone, Debug, Default, Eq, PartialEq)]
271#[cfg_attr(
272    feature = "derive",
273    derive(serde::Serialize, serde::Deserialize),
274    serde(rename_all = "kebab-case")
275)]
276pub struct ImapExtensionsConfig {
277    id: Option<ImapIdExtensionConfig>,
278}
279
280/// The IMAP configuration dedicated to the ID extension.
281///
282/// https://www.rfc-editor.org/rfc/rfc2971.html
283#[derive(Clone, Debug, Default, Eq, PartialEq)]
284#[cfg_attr(
285    feature = "derive",
286    derive(serde::Serialize, serde::Deserialize),
287    serde(rename_all = "kebab-case")
288)]
289pub struct ImapIdExtensionConfig {
290    /// Automatically sends the ID command straight after
291    /// authentication.
292    send_after_auth: Option<bool>,
293}