melib/imap: capability types WIP

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
imap-capabilities
Manos Pitsidianakis 3 months ago
parent 60cccdde2d
commit 250892a365
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -0,0 +1,113 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// 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 <http://www.gnu.org/licenses/>.
//
// 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<str>
+ 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 {
<Self as $crate::imap::capabilities::Capability>::NAME
}
}
impl ::serde::ser::Serialize for $ident {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: ::serde::ser::Serializer,
{
serializer.serialize_str(<Self as $crate::imap::capabilities::Capability>::NAME)
}
}
impl<'de> ::serde::de::Deserialize<'de> for $ident {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
if <&'_ str>::deserialize(deserializer)? == <Self as $crate::imap::capabilities::Capability>::NAME {
return Ok(Self);
}
Err(::serde::de::Error::custom(concat!("Expected string with value \"", $key, '"')))
}
}
impl AsRef<str> for $ident {
fn as_ref(&self) -> &'static str {
<Self as $crate::imap::capabilities::Capability>::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" }

@ -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())

@ -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))")

@ -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(

Loading…
Cancel
Save