Skip to main content

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