use imap::ConnectionMode;
use serde::{de, Deserialize, Deserializer, Serialize};
use std::{fmt, marker::PhantomData, result};
use thiserror::Error;
use crate::{
account::config::{oauth2::OAuth2Config, passwd::PasswdConfig},
Result,
};
#[derive(Debug, Error)]
pub enum Error {
#[error("cannot get imap password from global keyring")]
GetPasswdError(#[source] secret::Error),
#[error("cannot get imap password: password is empty")]
GetPasswdEmptyError,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ImapConfig {
pub host: String,
pub port: u16,
#[serde(default, deserialize_with = "some_bool_or_kind")]
pub encryption: Option<ImapEncryptionKind>,
pub login: String,
#[serde(flatten)]
pub auth: ImapAuthConfig,
pub watch: Option<ImapWatchConfig>,
}
impl ImapConfig {
pub fn is_encryption_enabled(&self) -> bool {
matches!(
self.encryption.as_ref(),
None | Some(ImapEncryptionKind::Tls) | Some(ImapEncryptionKind::StartTls)
)
}
pub fn is_start_tls_encryption_enabled(&self) -> bool {
matches!(self.encryption.as_ref(), Some(ImapEncryptionKind::StartTls))
}
pub fn is_encryption_disabled(&self) -> bool {
matches!(self.encryption.as_ref(), Some(ImapEncryptionKind::None))
}
pub async fn build_credentials(&self) -> Result<String> {
self.auth.build_credentials().await
}
pub fn find_watch_timeout(&self) -> Option<u64> {
self.watch.as_ref().and_then(|c| c.find_timeout())
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ImapEncryptionKind {
#[default]
#[serde(alias = "ssl")]
Tls,
#[serde(alias = "starttls")]
StartTls,
None,
}
impl fmt::Display for ImapEncryptionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Tls => write!(f, "SSL/TLS"),
Self::StartTls => write!(f, "StartTLS"),
Self::None => write!(f, "None"),
}
}
}
impl From<bool> for ImapEncryptionKind {
fn from(value: bool) -> Self {
if value {
Self::Tls
} else {
Self::None
}
}
}
impl From<ImapEncryptionKind> for ConnectionMode {
fn from(val: ImapEncryptionKind) -> Self {
match val {
ImapEncryptionKind::Tls => ConnectionMode::Tls,
ImapEncryptionKind::StartTls => ConnectionMode::StartTls,
ImapEncryptionKind::None => ConnectionMode::Plaintext,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImapAuthConfig {
#[serde(alias = "password")]
Passwd(PasswdConfig),
OAuth2(OAuth2Config),
}
impl Default for ImapAuthConfig {
fn default() -> Self {
Self::Passwd(Default::default())
}
}
impl ImapAuthConfig {
pub async fn build_credentials(&self) -> Result<String> {
match self {
ImapAuthConfig::Passwd(passwd) => {
let passwd = passwd.get().await.map_err(Error::GetPasswdError)?;
let passwd = passwd.lines().next().ok_or(Error::GetPasswdEmptyError)?;
Ok(passwd.to_owned())
}
ImapAuthConfig::OAuth2(oauth2) => Ok(oauth2.access_token().await?),
}
}
pub fn replace_undefined_keyring_entries(&mut self, name: impl AsRef<str>) {
let name = name.as_ref();
match self {
Self::Passwd(secret) => {
secret.set_keyring_entry_if_undefined(format!("{name}-imap-passwd"));
}
Self::OAuth2(config) => {
config
.client_secret
.set_keyring_entry_if_undefined(format!("{name}-imap-oauth2-client-secret"));
config
.access_token
.set_keyring_entry_if_undefined(format!("{name}-imap-oauth2-access-token"));
config
.refresh_token
.set_keyring_entry_if_undefined(format!("{name}-imap-oauth2-refresh-token"));
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ImapWatchConfig {
timeout: Option<u64>,
}
impl ImapWatchConfig {
pub fn find_timeout(&self) -> Option<u64> {
self.timeout
}
}
fn some_bool_or_kind<'de, D>(
deserializer: D,
) -> result::Result<Option<ImapEncryptionKind>, D::Error>
where
D: Deserializer<'de>,
{
struct SomeBoolOrKind(PhantomData<fn() -> Option<ImapEncryptionKind>>);
impl<'de> de::Visitor<'de> for SomeBoolOrKind {
type Value = Option<ImapEncryptionKind>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("some or none")
}
fn visit_some<D>(self, deserializer: D) -> result::Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct BoolOrKind(PhantomData<fn() -> ImapEncryptionKind>);
impl<'de> de::Visitor<'de> for BoolOrKind {
type Value = ImapEncryptionKind;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("boolean or string")
}
fn visit_bool<E>(self, v: bool) -> result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.into())
}
fn visit_str<E>(self, v: &str) -> result::Result<Self::Value, E>
where
E: de::Error,
{
Deserialize::deserialize(de::value::StrDeserializer::new(v))
}
}
deserializer
.deserialize_any(BoolOrKind(PhantomData))
.map(Option::Some)
}
}
deserializer.deserialize_option(SomeBoolOrKind(PhantomData))
}