Implement some IMAP extensions #50

Merged
quentin merged 15 commits from feat/more-ext into main 2024-01-04 11:11:02 +00:00
5 changed files with 75 additions and 59 deletions
Showing only changes of commit a6a0e1994d - Show all commits

View file

@ -1,5 +1,7 @@
use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::core::NonEmptyVec;
use imap_codec::imap_types::response::Capability; use imap_codec::imap_types::response::Capability;
use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind};
use std::collections::HashSet;
fn capability_unselect() -> Capability<'static> { fn capability_unselect() -> Capability<'static> {
Capability::try_from("UNSELECT").unwrap() Capability::try_from("UNSELECT").unwrap()
@ -14,84 +16,67 @@ fn capability_qresync() -> Capability<'static> {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServerCapability { pub struct ServerCapability(HashSet<Capability<'static>>);
r#move: bool,
unselect: bool,
condstore: bool,
qresync: bool,
}
impl Default for ServerCapability { impl Default for ServerCapability {
fn default() -> Self { fn default() -> Self {
Self { Self(HashSet::from([
r#move: true, Capability::Imap4Rev1,
unselect: true, Capability::Move,
condstore: false, capability_unselect(),
qresync: false, //capability_condstore(),
} //capability_qresync(),
]))
} }
} }
impl ServerCapability { impl ServerCapability {
pub fn to_vec(&self) -> NonEmptyVec<Capability<'static>> { pub fn to_vec(&self) -> NonEmptyVec<Capability<'static>> {
let mut acc = vec![Capability::Imap4Rev1]; self.0.iter().map(|v| v.clone()).collect::<Vec<_>>().try_into().unwrap()
if self.r#move {
acc.push(Capability::Move);
}
if self.unselect {
acc.push(capability_unselect());
}
if self.condstore {
acc.push(capability_condstore());
}
if self.qresync {
acc.push(capability_qresync());
}
acc.try_into().unwrap()
} }
#[allow(dead_code)]
pub fn support(&self, cap: &Capability<'static>) -> bool { pub fn support(&self, cap: &Capability<'static>) -> bool {
match cap { self.0.contains(cap)
Capability::Imap4Rev1 => true,
Capability::Move => self.r#move,
x if *x == capability_condstore() => self.condstore,
x if *x == capability_qresync() => self.qresync,
x if *x == capability_unselect() => self.unselect,
_ => false,
} }
} }
enum ClientStatus {
NotSupportedByServer,
Disabled,
Enabled,
} }
pub struct ClientCapability { pub struct ClientCapability {
condstore: bool, condstore: ClientStatus,
qresync: bool, utf8kind: Option<Utf8Kind>,
}
impl Default for ClientCapability {
fn default() -> Self {
Self {
condstore: false,
qresync: false,
}
}
} }
impl ClientCapability { impl ClientCapability {
pub fn new(sc: &ServerCapability) -> Self {
Self {
condstore: match sc.0.contains(&capability_condstore()) {
true => ClientStatus::Disabled,
_ => ClientStatus::NotSupportedByServer,
},
utf8kind: None,
}
}
pub fn try_enable( pub fn try_enable(
&mut self, &mut self,
srv: &ServerCapability, caps: &[CapabilityEnable<'static>],
caps: &[Capability<'static>], ) -> Vec<CapabilityEnable<'static>> {
) -> Vec<Capability<'static>> {
let mut enabled = vec![]; let mut enabled = vec![];
for cap in caps { for cap in caps {
match cap { match cap {
x if *x == capability_condstore() && srv.condstore && !self.condstore => { CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => {
self.condstore = true; self.condstore = ClientStatus::Enabled;
enabled.push(x.clone()); enabled.push(cap.clone());
} }
x if *x == capability_qresync() && srv.qresync && !self.qresync => { CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => {
self.qresync = true; self.utf8kind = Some(kind.clone());
enabled.push(x.clone()); enabled.push(cap.clone());
} }
_ => (), _ => (),
} }

View file

@ -3,14 +3,15 @@ use std::sync::Arc;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use imap_codec::imap_types::command::{Command, CommandBody}; use imap_codec::imap_types::command::{Command, CommandBody};
use imap_codec::imap_types::core::{Atom, Literal, QuotedChar}; use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, NonEmptyVec};
use imap_codec::imap_types::datetime::DateTime; use imap_codec::imap_types::datetime::DateTime;
use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute};
use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::response::{Code, CodeOther, Data};
use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
use imap_codec::imap_types::extensions::enable::CapabilityEnable;
use crate::imap::capability::ServerCapability; use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, MailboxName}; use crate::imap::command::{anystate, MailboxName};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
@ -24,6 +25,7 @@ use crate::mail::IMF;
pub struct AuthenticatedContext<'a> { pub struct AuthenticatedContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub server_capabilities: &'a ServerCapability, pub server_capabilities: &'a ServerCapability,
pub client_capabilities: &'a mut ClientCapability,
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
} }
@ -65,6 +67,11 @@ pub async fn dispatch<'a>(
message, message,
} => ctx.append(mailbox, flags, date, message).await, } => ctx.append(mailbox, flags, date, message).await,
// rfc5161 ENABLE
CommandBody::Enable { capabilities } => {
ctx.enable(capabilities)
},
// Collect other commands // Collect other commands
_ => anystate::wrong_state(ctx.req.tag.clone()), _ => anystate::wrong_state(ctx.req.tag.clone()),
} }
@ -511,6 +518,20 @@ impl<'a> AuthenticatedContext<'a> {
} }
} }
fn enable(self, cap_enable: &NonEmptyVec<CapabilityEnable<'static>>) -> Result<(Response<'static>, flow::Transition)> {
let mut response_builder = Response::build().to_req(self.req);
let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref());
if capabilities.len() > 0 {
response_builder = response_builder.data(Data::Enabled { capabilities });
}
Ok((
response_builder
.message("ENABLE completed")
.ok()?,
flow::Transition::None,
))
}
pub(crate) async fn append_internal( pub(crate) async fn append_internal(
self, self,
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,

View file

@ -7,7 +7,7 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet; use imap_codec::imap_types::sequence::SequenceSet;
use crate::imap::capability::ServerCapability; use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, authenticated}; use crate::imap::command::{anystate, authenticated};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
@ -19,6 +19,7 @@ pub struct ExaminedContext<'a> {
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
pub mailbox: &'a mut MailboxView, pub mailbox: &'a mut MailboxView,
pub server_capabilities: &'a ServerCapability, pub server_capabilities: &'a ServerCapability,
pub client_capabilities: &'a mut ClientCapability,
} }
pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> {
@ -60,6 +61,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl
authenticated::dispatch(authenticated::AuthenticatedContext { authenticated::dispatch(authenticated::AuthenticatedContext {
req: ctx.req, req: ctx.req,
server_capabilities: ctx.server_capabilities, server_capabilities: ctx.server_capabilities,
client_capabilities: ctx.client_capabilities,
user: ctx.user, user: ctx.user,
}) })
.await .await

View file

@ -10,7 +10,7 @@ use imap_codec::imap_types::response::{Code, CodeOther};
use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet; use imap_codec::imap_types::sequence::SequenceSet;
use crate::imap::capability::ServerCapability; use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::command::{anystate, authenticated, MailboxName};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
@ -23,6 +23,7 @@ pub struct SelectedContext<'a> {
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
pub mailbox: &'a mut MailboxView, pub mailbox: &'a mut MailboxView,
pub server_capabilities: &'a ServerCapability, pub server_capabilities: &'a ServerCapability,
pub client_capabilities: &'a mut ClientCapability,
} }
pub async fn dispatch<'a>( pub async fn dispatch<'a>(
@ -76,6 +77,7 @@ pub async fn dispatch<'a>(
authenticated::dispatch(authenticated::AuthenticatedContext { authenticated::dispatch(authenticated::AuthenticatedContext {
req: ctx.req, req: ctx.req,
server_capabilities: ctx.server_capabilities, server_capabilities: ctx.server_capabilities,
client_capabilities: ctx.client_capabilities,
user: ctx.user, user: ctx.user,
}) })
.await .await

View file

@ -1,4 +1,4 @@
use crate::imap::capability::ServerCapability; use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::command::{anonymous, authenticated, examined, selected};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::response::Response; use crate::imap::response::Response;
@ -9,14 +9,17 @@ use imap_codec::imap_types::command::Command;
pub struct Instance { pub struct Instance {
pub login_provider: ArcLoginProvider, pub login_provider: ArcLoginProvider,
pub server_capabilities: ServerCapability, pub server_capabilities: ServerCapability,
pub client_capabilities: ClientCapability,
pub state: flow::State, pub state: flow::State,
} }
impl Instance { impl Instance {
pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self {
let client_cap = ClientCapability::new(&cap);
Self { Self {
login_provider, login_provider,
state: flow::State::NotAuthenticated, state: flow::State::NotAuthenticated,
server_capabilities: cap, server_capabilities: cap,
client_capabilities: client_cap,
} }
} }
@ -36,6 +39,7 @@ impl Instance {
let ctx = authenticated::AuthenticatedContext { let ctx = authenticated::AuthenticatedContext {
req: &cmd, req: &cmd,
server_capabilities: &self.server_capabilities, server_capabilities: &self.server_capabilities,
client_capabilities: &mut self.client_capabilities,
user, user,
}; };
authenticated::dispatch(ctx).await authenticated::dispatch(ctx).await
@ -44,6 +48,7 @@ impl Instance {
let ctx = selected::SelectedContext { let ctx = selected::SelectedContext {
req: &cmd, req: &cmd,
server_capabilities: &self.server_capabilities, server_capabilities: &self.server_capabilities,
client_capabilities: &mut self.client_capabilities,
user, user,
mailbox, mailbox,
}; };
@ -53,6 +58,7 @@ impl Instance {
let ctx = examined::ExaminedContext { let ctx = examined::ExaminedContext {
req: &cmd, req: &cmd,
server_capabilities: &self.server_capabilities, server_capabilities: &self.server_capabilities,
client_capabilities: &mut self.client_capabilities,
user, user,
mailbox, mailbox,
}; };