From 250892a36570e0b229b317854a35181273dbc3b5 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 2 Jun 2024 14:35:08 +0300 Subject: [PATCH] melib/imap: capability types WIP Signed-off-by: Manos Pitsidianakis --- melib/src/imap/capabilities.rs | 113 +++++++++++++++++++++++++++++++++ melib/src/imap/connection.rs | 18 +++--- melib/src/imap/mod.rs | 77 +++++++++++----------- melib/src/imap/watch.rs | 2 +- 4 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 melib/src/imap/capabilities.rs diff --git a/melib/src/imap/capabilities.rs b/melib/src/imap/capabilities.rs new file mode 100644 index 00000000..ede7346c --- /dev/null +++ b/melib/src/imap/capabilities.rs @@ -0,0 +1,113 @@ +// +// meli +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of meli. +// +// meli is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// meli is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with meli. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +use serde::{de::DeserializeOwned, ser::Serialize}; + +pub trait Capability: + Clone + + Copy + + std::fmt::Debug + + PartialEq + + Eq + + std::hash::Hash + + Serialize + + DeserializeOwned + + AsRef + + Send + + Sync +{ + const NAME: &'static str; +} + +#[macro_export] +macro_rules! _impl_imap_capability { + ($(#[$outer:meta])*$ident:ident : $key:literal) => { + $crate::_impl_imap_capability! { + $(#[$outer])*$ident: $key, name: $key + } + }; + ($(#[$outer:meta])*$ident:ident : $key:literal, name: $name:literal) => { + $(#[$outer])* + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] + pub struct $ident; + + impl $crate::imap::capabilities::Capability for $ident { + const NAME: &'static str = $name; + } + + impl $ident { + pub const fn name() -> &'static str { + ::NAME + } + } + + impl ::serde::ser::Serialize for $ident { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: ::serde::ser::Serializer, + { + serializer.serialize_str(::NAME) + } + } + + impl<'de> ::serde::de::Deserialize<'de> for $ident { + fn deserialize(deserializer: D) -> std::result::Result + where + D: ::serde::de::Deserializer<'de>, + { + if <&'_ str>::deserialize(deserializer)? == ::NAME { + return Ok(Self); + } + + Err(::serde::de::Error::custom(concat!("Expected string with value \"", $key, '"'))) + } + } + + impl AsRef for $ident { + fn as_ref(&self) -> &'static str { + ::NAME + } + } + + impl ::std::fmt::Display for $ident { + fn fmt(&self, fmt: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(fmt, "IMAP {} Capability", Self::name()) + } + } + }; +} + +_impl_imap_capability! { Idle: "IDLE" } +_impl_imap_capability! { Condstore: "CONDSTORE" } +_impl_imap_capability! { Deflate: "COMPRESS=DEFLATE" } +_impl_imap_capability! { OAuth2: "AUTH=OAUTH2" } +_impl_imap_capability! { XOAuth2: "AUTH=XOAUTH2" } +_impl_imap_capability! { Enable: "ENABLE" } +_impl_imap_capability! { IMAP4Rev1: "IMAP4REV1" } +_impl_imap_capability! { ListExtended: "LIST-EXTENDED" } +_impl_imap_capability! { ListStatus: "LIST-STATUS" } +_impl_imap_capability! { LiteralPlus: "LITERAL+" } +_impl_imap_capability! { Login: "LOGIN" } +_impl_imap_capability! { LoginDisabled: "LOGINDISABLED" } +_impl_imap_capability! { Move: "MOVE" } +_impl_imap_capability! { SpecialUse: "SPECIAL-USE" } +_impl_imap_capability! { Unselect: "UNSELECT" } diff --git a/melib/src/imap/connection.rs b/melib/src/imap/connection.rs index 690338a8..04528c02 100644 --- a/melib/src/imap/connection.rs +++ b/melib/src/imap/connection.rs @@ -24,8 +24,9 @@ use crate::{ email::parser::BytesExt, error::*, imap::{ + capabilities, protocol_parser::{self, ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse}, - Capabilities, ImapServerConf, UIDStore, + Capabilities, Capability, ImapServerConf, UIDStore, }, text::Truncate, utils::{ @@ -370,7 +371,7 @@ impl ImapStream { if !capabilities .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"IMAP4rev1")) + .any(|cap| cap.eq_ignore_ascii_case(capabilities::IMAP4Rev1::NAME.as_bytes())) { return Err(Error::new(format!( "Could not connect to {}: server is not IMAP4rev1 compliant", @@ -379,7 +380,7 @@ impl ImapStream { .set_kind(ErrorKind::ProtocolNotSupported)); } else if capabilities .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"LOGINDISABLED")) + .any(|cap| cap.eq_ignore_ascii_case(capabilities::LoginDisabled::NAME.as_bytes())) { return Err(Error::new(format!( "Could not connect to {}: server does not accept logins [LOGINDISABLED]", @@ -400,7 +401,7 @@ impl ImapStream { } if oauth2 => { if !capabilities .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"AUTH=XOAUTH2")) + .any(|cap| cap.eq_ignore_ascii_case(capabilities::XOAuth2::NAME.as_bytes())) { return Err(Error::new(format!( "Could not connect to {}: OAUTH2 is enabled but server did not return \ @@ -722,13 +723,14 @@ impl ImapConnection { oauth2: _, }, } => { - if capabilities.contains(&b"CONDSTORE"[..]) && condstore { + if capabilities.contains(capabilities::Condstore::NAME.as_bytes()) && condstore + { match self.sync_policy { SyncPolicy::None => { /* do nothing, sync is disabled */ } _ => { /* Upgrade to Condstore */ let mut ret = Vec::new(); - if capabilities.contains(&b"ENABLE"[..]) { + if capabilities.contains(capabilities::Enable::NAME.as_bytes()) { self.send_command(CommandBody::Enable { capabilities: NonEmptyVec::from( CapabilityEnable::CondStore, @@ -757,7 +759,7 @@ impl ImapConnection { } } } - if capabilities.contains(&b"COMPRESS=DEFLATE"[..]) && deflate { + if capabilities.contains(capabilities::Deflate::NAME.as_bytes()) && deflate { let mut ret = Vec::new(); self.send_command(CommandBody::compress(CompressionAlgorithm::Deflate)) .await?; @@ -1122,7 +1124,7 @@ impl ImapConnection { .lock() .unwrap() .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT")) + .any(|cap| cap.eq_ignore_ascii_case(capabilities::Unselect::NAME.as_bytes())) { self.send_command(CommandBody::Unselect).await?; self.read_response(&mut response, RequiredResponses::empty()) diff --git a/melib/src/imap/mod.rs b/melib/src/imap/mod.rs index ca147887..7322103b 100644 --- a/melib/src/imap/mod.rs +++ b/melib/src/imap/mod.rs @@ -38,10 +38,13 @@ pub use watch::*; mod search; pub use search::*; pub mod cache; +pub mod capabilities; pub mod error; pub mod managesieve; pub mod untagged; +use capabilities::Capability; + use std::{ collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet}, convert::TryFrom, @@ -80,20 +83,20 @@ pub type UIDVALIDITY = UID; pub type MessageSequenceNumber = ImapNum; pub static SUPPORTED_CAPABILITIES: &[&str] = &[ - "AUTH=OAUTH2", - "COMPRESS=DEFLATE", - "CONDSTORE", - "ENABLE", - "IDLE", - "IMAP4REV1", - "LIST-EXTENDED", - "LIST-STATUS", - "LITERAL+", - "LOGIN", - "LOGINDISABLED", - "MOVE", - "SPECIAL-USE", - "UNSELECT", + capabilities::OAuth2::NAME, + capabilities::Deflate::NAME, + capabilities::Condstore::NAME, + capabilities::Enable::NAME, + capabilities::Idle::NAME, + capabilities::IMAP4Rev1::NAME, + capabilities::ListExtended::NAME, + capabilities::ListStatus::NAME, + capabilities::LiteralPlus::NAME, + capabilities::Login::NAME, + capabilities::LoginDisabled::NAME, + capabilities::Move::NAME, + capabilities::SpecialUse::NAME, + capabilities::Unselect::NAME, ]; #[derive(Debug, Default)] @@ -263,7 +266,7 @@ impl MailBackend for ImapType { { for (name, status) in extensions.iter_mut() { match name.as_str() { - "IDLE" => { + capabilities::Idle::NAME => { if idle { *status = MailBackendExtensionStatus::Enabled { comment: None }; } else { @@ -272,7 +275,7 @@ impl MailBackend for ImapType { }; } } - "COMPRESS=DEFLATE" => { + capabilities::Deflate::NAME => { if deflate { *status = MailBackendExtensionStatus::Enabled { comment: None }; } else { @@ -281,7 +284,7 @@ impl MailBackend for ImapType { }; } } - "CONDSTORE" => { + capabilities::Condstore::NAME => { if condstore { *status = MailBackendExtensionStatus::Enabled { comment: None }; } else { @@ -290,7 +293,7 @@ impl MailBackend for ImapType { }; } } - "AUTH=OAUTH2" => { + capabilities::OAuth2::NAME => { if oauth2 { *status = MailBackendExtensionStatus::Enabled { comment: None }; } else { @@ -494,19 +497,17 @@ impl MailBackend for ImapType { let main_conn = self.connection.clone(); let uid_store = self.uid_store.clone(); Ok(Box::pin(async move { - let has_idle: bool = match server_conf.protocol { - ImapProtocol::IMAP { - extension_use: ImapExtensionUse { idle, .. }, - } => { - idle && uid_store - .capabilities - .lock() - .unwrap() - .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"IDLE")) - } - _ => false, - }; + let has_idle: bool = + match server_conf.protocol { + ImapProtocol::IMAP { + extension_use: ImapExtensionUse { idle, .. }, + } => { + idle && uid_store.capabilities.lock().unwrap().iter().any(|cap| { + cap.eq_ignore_ascii_case(capabilities::Idle::NAME.as_bytes()) + }) + } + _ => false, + }; while let Err(err) = if has_idle { idle(ImapWatchKit { conn: ImapConnection::new_connection( @@ -620,12 +621,10 @@ impl MailBackend for ImapType { mailbox.imap_path().to_string() }; let flags = flags.unwrap_or_else(Flag::empty); - let has_literal_plus: bool = uid_store - .capabilities - .lock() - .unwrap() - .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"LITERAL+")); + let has_literal_plus: bool = + uid_store.capabilities.lock().unwrap().iter().any(|cap| { + cap.eq_ignore_ascii_case(capabilities::LiteralPlus::NAME.as_bytes()) + }); let data = if has_literal_plus { Literal::try_from(bytes)?.into_non_sync() } else { @@ -659,7 +658,7 @@ impl MailBackend for ImapType { .lock() .unwrap() .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"MOVE")); + .any(|cap| cap.eq_ignore_ascii_case(capabilities::Move::NAME.as_bytes())); Ok(Box::pin(async move { let uids: SmallVec<[UID; 64]> = { let hash_index_lck = uid_store.hash_index.lock().unwrap(); @@ -1407,7 +1406,7 @@ impl ImapType { .lock() .unwrap() .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS")); + .any(|cap| cap.eq_ignore_ascii_case(capabilities::ListStatus::NAME.as_bytes())); if has_list_status { // [ref:TODO]: (#222) imap-codec does not support "LIST Command Extensions" currently. conn.send_command_raw(b"LIST \"\" \"*\" RETURN (STATUS (MESSAGES UNSEEN))") diff --git a/melib/src/imap/watch.rs b/melib/src/imap/watch.rs index 0da6a4fa..4bcc3a1d 100644 --- a/melib/src/imap/watch.rs +++ b/melib/src/imap/watch.rs @@ -247,7 +247,7 @@ pub async fn examine_updates( .lock() .unwrap() .iter() - .any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS")); + .any(|cap| cap.eq_ignore_ascii_case(capabilities::ListStatus::NAME.as_bytes())); if has_list_status { // [ref:TODO]: (#222) imap-codec does not support "LIST Command Extensions" currently. conn.send_command_raw(