From 7ebc708acab9c91db41652cfbfe2814a3a27569d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 09:21:46 +0100 Subject: unselect implemented rfc3691 --- src/imap/command/anystate.rs | 7 +++++-- src/imap/command/examined.rs | 11 +++++++---- src/imap/command/selected.rs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) (limited to 'src/imap') diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 42fe645..f326852 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -6,8 +6,11 @@ use crate::imap::flow; use crate::imap::response::Response; pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - let capabilities: NonEmptyVec = - (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let capabilities: NonEmptyVec = (vec![ + Capability::Imap4Rev1, + Capability::try_from("UNSELECT").unwrap(), + ]) + .try_into()?; let res = Response::build() .tag(tag) .message("Server capabilities") diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7de94f4..8876297 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -28,7 +28,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl // Specific to the EXAMINE state (specialization of the SELECTED state) // ~3 commands -> close, fetch, search + NOOP - CommandBody::Close => ctx.close().await, + CommandBody::Close => ctx.close("CLOSE").await, CommandBody::Fetch { sequence_set, macro_or_item_names, @@ -44,10 +44,13 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl Response::build() .to_req(ctx.req) .message("Forbidden command: can't write in read-only mode (EXAMINE)") - .bad()?, + .no()?, flow::Transition::None, )), + // UNSELECT extension (rfc3691) + CommandBody::Unselect => ctx.close("UNSELECT").await, + // In examined mode, we fallback to authenticated when needed _ => { authenticated::dispatch(authenticated::AuthenticatedContext { @@ -64,11 +67,11 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl impl<'a> ExaminedContext<'a> { /// CLOSE in examined state is not the same as in selected state /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn close(self) -> Result<(Response<'static>, flow::Transition)> { + async fn close(self, kind: &str) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) - .message("CLOSE completed") + .message(format!("{} completed", kind)) .ok()?, flow::Transition::Unselect, )) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 220a952..0653226 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -59,6 +59,9 @@ pub async fn dispatch<'a>( uid, } => ctx.copy(sequence_set, mailbox, uid).await, + // UNSELECT extension (rfc3691) + CommandBody::Unselect => ctx.unselect().await, + // In selected mode, we fallback to authenticated when needed _ => { authenticated::dispatch(authenticated::AuthenticatedContext { @@ -84,6 +87,16 @@ impl<'a> SelectedContext<'a> { )) } + async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("UNSELECT completed") + .ok()?, + flow::Transition::Unselect, + )) + } + pub async fn fetch( self, sequence_set: &SequenceSet, -- cgit v1.2.3 From 6d37924399dd5d04f5be2506e3e044dd165f6399 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 12:29:19 +0100 Subject: rework capability --- src/imap/capability.rs | 30 ++++++++++++++++++++++++++++++ src/imap/command/anonymous.rs | 8 +++++++- src/imap/command/anystate.rs | 14 +++++--------- src/imap/command/authenticated.rs | 6 +++++- src/imap/command/examined.rs | 8 +++++++- src/imap/command/selected.rs | 8 +++++++- src/imap/mod.rs | 12 +++++++++--- src/imap/session.rs | 13 +++++++++++-- 8 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/imap/capability.rs (limited to 'src/imap') diff --git a/src/imap/capability.rs b/src/imap/capability.rs new file mode 100644 index 0000000..b98e8f8 --- /dev/null +++ b/src/imap/capability.rs @@ -0,0 +1,30 @@ +use imap_codec::imap_types::core::NonEmptyVec; +use imap_codec::imap_types::response::Capability; + +#[derive(Debug, Clone)] +pub struct ServerCapability { + r#move: bool, + unselect: bool, +} + +impl Default for ServerCapability { + fn default() -> Self { + Self { + r#move: true, + unselect: true, + } + } +} + +impl ServerCapability { + pub fn to_vec(&self) -> NonEmptyVec> { + let mut acc = vec![Capability::Imap4Rev1]; + if self.r#move { + acc.push(Capability::Move); + } + if self.unselect { + acc.push(Capability::try_from("UNSELECT").unwrap()); + } + acc.try_into().unwrap() + } +} diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index fbd10e9..fda78c2 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,10 +1,12 @@ use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody}; use imap_codec::imap_types::core::AString; +use imap_codec::imap_types::response::Code; use imap_codec::imap_types::secret::Secret; use crate::imap::command::anystate; use crate::imap::flow; +use crate::imap::capability::ServerCapability; use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -13,6 +15,7 @@ use crate::mail::user::User; pub struct AnonymousContext<'a> { pub req: &'a Command<'static>, + pub server_capabilities: &'a ServerCapability, pub login_provider: &'a ArcLoginProvider, } @@ -20,7 +23,9 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, f match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities), CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) @@ -69,6 +74,7 @@ impl<'a> AnonymousContext<'a> { Ok(( Response::build() .to_req(self.req) + .code(Code::Capability(self.server_capabilities.to_vec())) .message("Completed") .ok()?, flow::Transition::Authenticate(user), diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index f326852..7d7c0b2 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -1,20 +1,16 @@ use anyhow::Result; -use imap_codec::imap_types::core::{NonEmptyVec, Tag}; -use imap_codec::imap_types::response::{Capability, Data}; +use imap_codec::imap_types::core::Tag; +use imap_codec::imap_types::response::Data; use crate::imap::flow; +use crate::imap::capability::ServerCapability; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - let capabilities: NonEmptyVec = (vec![ - Capability::Imap4Rev1, - Capability::try_from("UNSELECT").unwrap(), - ]) - .try_into()?; +pub(crate) fn capability(tag: Tag<'static>, cap: &ServerCapability) -> Result<(Response<'static>, flow::Transition)> { let res = Response::build() .tag(tag) .message("Server capabilities") - .data(Data::Capability(capabilities)) + .data(Data::Capability(cap.to_vec())) .ok()?; Ok((res, flow::Transition::None)) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 1bb4c6d..a6a5203 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -14,6 +14,7 @@ use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; @@ -22,6 +23,7 @@ use crate::mail::IMF; pub struct AuthenticatedContext<'a> { pub req: &'a Command<'static>, + pub server_capabilities: &'a ServerCapability, pub user: &'a Arc, } @@ -31,7 +33,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities), CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 8876297..7cccf7b 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -11,19 +11,24 @@ use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::mail::user::User; pub struct ExaminedContext<'a> { pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, + pub server_capabilities: &'a ServerCapability, } pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities, + ), CommandBody::Logout => anystate::logout(), // Specific to the EXAMINE state (specialization of the SELECTED state) @@ -55,6 +60,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl _ => { authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, + server_capabilities: ctx.server_capabilities, user: ctx.user, }) .await diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 0653226..681b509 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -14,6 +14,7 @@ use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::mail::user::User; @@ -21,6 +22,7 @@ pub struct SelectedContext<'a> { pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, + pub server_capabilities: &'a ServerCapability, } pub async fn dispatch<'a>( @@ -29,7 +31,10 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities, + ), CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) @@ -66,6 +71,7 @@ pub async fn dispatch<'a>( _ => { authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, + server_capabilities: ctx.server_capabilities, user: ctx.user, }) .await diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 31eeaa8..693c99a 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -3,6 +3,7 @@ mod flow; mod mailbox_view; mod response; mod session; +mod capability; use std::net::SocketAddr; @@ -17,12 +18,14 @@ use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; use imap_flow::stream::AnyStream; use crate::config::ImapConfig; +use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; /// Server is a thin wrapper to register our Services in BàL pub struct Server { bind_addr: SocketAddr, login_provider: ArcLoginProvider, + capabilities: ServerCapability, } struct ClientContext { @@ -30,12 +33,14 @@ struct ClientContext { addr: SocketAddr, login_provider: ArcLoginProvider, must_exit: watch::Receiver, + server_capabilities: ServerCapability, } pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server { Server { bind_addr: config.bind_addr, login_provider: login, + capabilities: ServerCapability::default(), } } @@ -66,6 +71,7 @@ impl Server { addr: remote_addr.clone(), login_provider: self.login_provider.clone(), must_exit: must_exit.clone(), + server_capabilities: self.capabilities.clone(), }; let conn = tokio::spawn(client_wrapper(client)); connections.push(conn); @@ -96,21 +102,21 @@ async fn client(mut ctx: ClientContext) -> Result<()> { let (mut server, _) = ServerFlow::send_greeting( ctx.stream, ServerFlowOptions::default(), - Greeting::ok(None, "Aerogramme").unwrap(), + Greeting::ok(Some(Code::Capability(ctx.server_capabilities.to_vec())), "Aerogramme").unwrap(), ) .await?; use crate::imap::response::{Body, Response as MyResponse}; use crate::imap::session::Instance; use imap_codec::imap_types::command::Command; - use imap_codec::imap_types::response::{Response, Status}; + use imap_codec::imap_types::response::{Response, Code, Status}; use tokio::sync::mpsc; let (cmd_tx, mut cmd_rx) = mpsc::channel::>(10); let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::>(); let bckgrnd = tokio::spawn(async move { - let mut session = Instance::new(ctx.login_provider); + let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities); loop { let cmd = match cmd_rx.recv().await { None => break, diff --git a/src/imap/session.rs b/src/imap/session.rs index 5c67f8e..c4f062f 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,19 +1,22 @@ use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; use imap_codec::imap_types::command::Command; //----- pub struct Instance { pub login_provider: ArcLoginProvider, + pub server_capabilities: ServerCapability, pub state: flow::State, } impl Instance { - pub fn new(login_provider: ArcLoginProvider) -> Self { + pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { Self { login_provider, state: flow::State::NotAuthenticated, + server_capabilities: cap, } } @@ -25,16 +28,21 @@ impl Instance { let ctx = anonymous::AnonymousContext { req: &cmd, login_provider: &self.login_provider, + server_capabilities: &self.server_capabilities, }; anonymous::dispatch(ctx).await } flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { req: &cmd, user }; + let ctx = authenticated::AuthenticatedContext { + req: &cmd, + server_capabilities: &self.server_capabilities, + user }; authenticated::dispatch(ctx).await } flow::State::Selected(ref user, ref mut mailbox) => { let ctx = selected::SelectedContext { req: &cmd, + server_capabilities: &self.server_capabilities, user, mailbox, }; @@ -43,6 +51,7 @@ impl Instance { flow::State::Examined(ref user, ref mut mailbox) => { let ctx = examined::ExaminedContext { req: &cmd, + server_capabilities: &self.server_capabilities, user, mailbox, }; -- cgit v1.2.3 From ef257e286a5377031bfde9645629825d3223b3d4 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 15:00:05 +0100 Subject: implement move --- src/imap/command/anonymous.rs | 8 ++--- src/imap/command/anystate.rs | 7 ++-- src/imap/command/authenticated.rs | 8 ++--- src/imap/command/examined.rs | 9 +++--- src/imap/command/selected.rs | 68 ++++++++++++++++++++++++++++++++++++--- src/imap/mailbox_view.rs | 31 ++++++++++++++++++ src/imap/mod.rs | 10 ++++-- src/imap/session.rs | 9 +++--- 8 files changed, 123 insertions(+), 27 deletions(-) (limited to 'src/imap') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index fda78c2..0582b06 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -4,9 +4,9 @@ use imap_codec::imap_types::core::AString; use imap_codec::imap_types::response::Code; use imap_codec::imap_types::secret::Secret; +use crate::imap::capability::ServerCapability; use crate::imap::command::anystate; use crate::imap::flow; -use crate::imap::capability::ServerCapability; use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -23,9 +23,9 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, f match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 7d7c0b2..718ba3f 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -2,11 +2,14 @@ use anyhow::Result; use imap_codec::imap_types::core::Tag; use imap_codec::imap_types::response::Data; -use crate::imap::flow; use crate::imap::capability::ServerCapability; +use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag<'static>, cap: &ServerCapability) -> Result<(Response<'static>, flow::Transition)> { +pub(crate) fn capability( + tag: Tag<'static>, + cap: &ServerCapability, +) -> Result<(Response<'static>, flow::Transition)> { let res = Response::build() .tag(tag) .message("Server capabilities") diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index a6a5203..2970b63 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -10,11 +10,11 @@ use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; @@ -33,9 +33,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7cccf7b..bddd0f9 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,11 +7,11 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::user::User; pub struct ExaminedContext<'a> { @@ -25,10 +25,9 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities, - ), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to the EXAMINE state (specialization of the SELECTED state) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 681b509..28ebbe8 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,11 +10,11 @@ use imap_codec::imap_types::response::{Code, CodeOther}; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::user::User; @@ -31,10 +31,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities, - ), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) @@ -63,6 +62,11 @@ pub async fn dispatch<'a>( mailbox, uid, } => ctx.copy(sequence_set, mailbox, uid).await, + CommandBody::Move { + sequence_set, + mailbox, + uid, + } => ctx.r#move(sequence_set, mailbox, uid).await, // UNSELECT extension (rfc3691) CommandBody::Unselect => ctx.unselect().await, @@ -245,4 +249,58 @@ impl<'a> SelectedContext<'a> { flow::Transition::None, )) } + + async fn r#move( + self, + sequence_set: &SequenceSet, + mailbox: &MailboxCodec<'a>, + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .no()?, + flow::Transition::None, + )) + } + }; + + let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; + + // compute code + let copyuid_str = format!( + "{} {} {}", + uidval, + uid_map + .iter() + .map(|(sid, _)| format!("{}", sid)) + .collect::>() + .join(","), + uid_map + .iter() + .map(|(_, tuid)| format!("{}", tuid)) + .collect::>() + .join(",") + ); + + Ok(( + Response::build() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .set_body(data) + .ok()?, + flow::Transition::None, + )) + } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 7434512..206b905 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -600,6 +600,37 @@ impl MailboxView { Ok((to_state.uidvalidity, ret)) } + pub async fn r#move( + &mut self, + sequence_set: &SequenceSet, + to: Arc, + is_uid_copy: &bool, + ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec>)> { + let mails = self.get_mail_ids(sequence_set, *is_uid_copy)?; + + let mut new_uuids = vec![]; + for mi in mails.iter() { + let copy_action = to.copy_from(&self.mailbox, mi.uuid).await?; + new_uuids.push(copy_action); + self.mailbox.delete(mi.uuid).await? + } + + let mut ret = vec![]; + let to_state = to.current_uid_index().await; + for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { + let dest_uid = to_state + .table + .get(new_uuid) + .ok_or(anyhow!("moved mail not in destination mailbox"))? + .0; + ret.push((mi.uid, dest_uid)); + } + + let update = self.update().await?; + + Ok((to_state.uidvalidity, ret, update)) + } + /// Looks up state changes in the mailbox and produces a set of IMAP /// responses describing the new state. pub async fn fetch<'b>( diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 693c99a..0b5555a 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,9 +1,9 @@ +mod capability; mod command; mod flow; mod mailbox_view; mod response; mod session; -mod capability; use std::net::SocketAddr; @@ -102,14 +102,18 @@ async fn client(mut ctx: ClientContext) -> Result<()> { let (mut server, _) = ServerFlow::send_greeting( ctx.stream, ServerFlowOptions::default(), - Greeting::ok(Some(Code::Capability(ctx.server_capabilities.to_vec())), "Aerogramme").unwrap(), + Greeting::ok( + Some(Code::Capability(ctx.server_capabilities.to_vec())), + "Aerogramme", + ) + .unwrap(), ) .await?; use crate::imap::response::{Body, Response as MyResponse}; use crate::imap::session::Instance; use imap_codec::imap_types::command::Command; - use imap_codec::imap_types::response::{Response, Code, Status}; + use imap_codec::imap_types::response::{Code, Response, Status}; use tokio::sync::mpsc; let (cmd_tx, mut cmd_rx) = mpsc::channel::>(10); diff --git a/src/imap/session.rs b/src/imap/session.rs index c4f062f..55026b9 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,7 +1,7 @@ +use crate::imap::capability::ServerCapability; use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; use imap_codec::imap_types::command::Command; @@ -33,10 +33,11 @@ impl Instance { anonymous::dispatch(ctx).await } flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &cmd, + let ctx = authenticated::AuthenticatedContext { + req: &cmd, server_capabilities: &self.server_capabilities, - user }; + user, + }; authenticated::dispatch(ctx).await } flow::State::Selected(ref user, ref mut mailbox) => { -- cgit v1.2.3 From 74686ebb778b740ccfccfbf61ccd24628f60e9d0 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 16:52:31 +0100 Subject: append ignore dates instead of failing --- src/imap/capability.rs | 74 ++++++++++++++++++++++++++++++++++++++- src/imap/command/authenticated.rs | 5 ++- src/imap/mod.rs | 4 ++- 3 files changed, 80 insertions(+), 3 deletions(-) (limited to 'src/imap') diff --git a/src/imap/capability.rs b/src/imap/capability.rs index b98e8f8..f6b5a51 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,10 +1,24 @@ use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::response::Capability; +fn capability_unselect() -> Capability<'static> { + Capability::try_from("UNSELECT").unwrap() +} + +fn capability_condstore() -> Capability<'static> { + Capability::try_from("CONDSTORE").unwrap() +} + +fn capability_qresync() -> Capability<'static> { + Capability::try_from("QRESYNC").unwrap() +} + #[derive(Debug, Clone)] pub struct ServerCapability { r#move: bool, unselect: bool, + condstore: bool, + qresync: bool, } impl Default for ServerCapability { @@ -12,6 +26,8 @@ impl Default for ServerCapability { Self { r#move: true, unselect: true, + condstore: false, + qresync: false, } } } @@ -23,8 +39,64 @@ impl ServerCapability { acc.push(Capability::Move); } if self.unselect { - acc.push(Capability::try_from("UNSELECT").unwrap()); + acc.push(capability_unselect()); + } + if self.condstore { + acc.push(capability_condstore()); + } + if self.qresync { + acc.push(capability_qresync()); } acc.try_into().unwrap() } + + pub fn support(&self, cap: &Capability<'static>) -> bool { + match 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, + } + } +} + +pub struct ClientCapability { + condstore: bool, + qresync: bool, +} + +impl Default for ClientCapability { + fn default() -> Self { + Self { + condstore: false, + qresync: false, + } + } +} + +impl ClientCapability { + pub fn try_enable( + &mut self, + srv: &ServerCapability, + caps: &[Capability<'static>], + ) -> Vec> { + let mut enabled = vec![]; + for cap in caps { + match cap { + x if *x == capability_condstore() && srv.condstore && !self.condstore => { + self.condstore = true; + enabled.push(x.clone()); + } + x if *x == capability_qresync() && srv.qresync && !self.qresync => { + self.qresync = true; + enabled.push(x.clone()); + } + _ => (), + } + } + + enabled + } } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 2970b63..fbf29f9 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -305,6 +305,9 @@ impl<'a> AuthenticatedContext<'a> { StatusDataItemName::DeletedStorage => { bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); }, + StatusDataItemName::HighestModSeq => { + bail!("highestmodseq not yet implemented"); + } }); } @@ -524,7 +527,7 @@ impl<'a> AuthenticatedContext<'a> { }; if date.is_some() { - bail!("Cannot set date when appending message"); + tracing::warn!("Cannot set date when appending message"); } let msg = diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 0b5555a..aac1fd3 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -89,7 +89,7 @@ async fn client_wrapper(ctx: ClientContext) { let addr = ctx.addr.clone(); match client(ctx).await { Ok(()) => { - tracing::info!("closing successful session for {:?}", addr); + tracing::debug!("closing successful session for {:?}", addr); } Err(e) => { tracing::error!("closing errored session for {:?}: {}", addr, e); @@ -127,7 +127,9 @@ async fn client(mut ctx: ClientContext) -> Result<()> { Some(cmd_recv) => cmd_recv, }; + tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command"); let maybe_response = session.command(cmd).await; + tracing::debug!(cmd=?maybe_response.completion, sock=%ctx.addr, "response"); match resp_tx.send(maybe_response) { Err(_) => break, -- cgit v1.2.3 From 7de1c66d86dbfe8e8ef4ed1183e5a9b6a3a636bf Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 18:25:37 +0100 Subject: Thunderbird is now able to correctly list msg info --- src/imap/mailbox_view.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/imap') diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 206b905..861d27c 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1273,7 +1273,11 @@ fn get_message_section<'a>( part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]), |part_msg| { let mut ret = vec![]; - for f in &part_msg.mime().kv { + let mime = match &part_msg { + AnyPart::Msg(msg) => msg.child.mime(), + other => other.mime(), + }; + for f in mime.kv.iter() { let (k, v) = match f { header::Field::Good(header::Kv2(k, v)) => (k, v), _ => continue, @@ -1303,7 +1307,7 @@ fn get_message_section<'a>( let bytes = match &part { AnyPart::Txt(p) => p.mime.fields.raw, AnyPart::Bin(p) => p.mime.fields.raw, - AnyPart::Msg(p) => p.mime.fields.raw, + AnyPart::Msg(p) => p.child.mime().raw, AnyPart::Mult(p) => p.mime.fields.raw, }; Ok(bytes.to_vec().into()) -- cgit v1.2.3 From a6a0e1994db2b43f559e5c2e8ae5c58342e5136f Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 20:53:07 +0100 Subject: ENABLE is now supported --- src/imap/capability.rs | 87 ++++++++++++++++----------------------- src/imap/command/authenticated.rs | 25 ++++++++++- src/imap/command/examined.rs | 4 +- src/imap/command/selected.rs | 4 +- src/imap/session.rs | 8 +++- 5 files changed, 72 insertions(+), 56 deletions(-) (limited to 'src/imap') diff --git a/src/imap/capability.rs b/src/imap/capability.rs index f6b5a51..3929558 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,5 +1,7 @@ use imap_codec::imap_types::core::NonEmptyVec; 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> { Capability::try_from("UNSELECT").unwrap() @@ -14,84 +16,67 @@ fn capability_qresync() -> Capability<'static> { } #[derive(Debug, Clone)] -pub struct ServerCapability { - r#move: bool, - unselect: bool, - condstore: bool, - qresync: bool, -} +pub struct ServerCapability(HashSet>); impl Default for ServerCapability { fn default() -> Self { - Self { - r#move: true, - unselect: true, - condstore: false, - qresync: false, - } + Self(HashSet::from([ + Capability::Imap4Rev1, + Capability::Move, + capability_unselect(), + //capability_condstore(), + //capability_qresync(), + ])) } } impl ServerCapability { pub fn to_vec(&self) -> NonEmptyVec> { - let mut acc = vec![Capability::Imap4Rev1]; - 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() + self.0.iter().map(|v| v.clone()).collect::>().try_into().unwrap() } + #[allow(dead_code)] pub fn support(&self, cap: &Capability<'static>) -> bool { - match 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, - } + self.0.contains(cap) } } +enum ClientStatus { + NotSupportedByServer, + Disabled, + Enabled, +} + pub struct ClientCapability { - condstore: bool, - qresync: bool, + condstore: ClientStatus, + utf8kind: Option, } -impl Default for ClientCapability { - fn default() -> Self { +impl ClientCapability { + pub fn new(sc: &ServerCapability) -> Self { Self { - condstore: false, - qresync: false, + condstore: match sc.0.contains(&capability_condstore()) { + true => ClientStatus::Disabled, + _ => ClientStatus::NotSupportedByServer, + }, + utf8kind: None, } } -} -impl ClientCapability { pub fn try_enable( &mut self, - srv: &ServerCapability, - caps: &[Capability<'static>], - ) -> Vec> { + caps: &[CapabilityEnable<'static>], + ) -> Vec> { let mut enabled = vec![]; for cap in caps { match cap { - x if *x == capability_condstore() && srv.condstore && !self.condstore => { - self.condstore = true; - enabled.push(x.clone()); + CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => { + self.condstore = ClientStatus::Enabled; + enabled.push(cap.clone()); } - x if *x == capability_qresync() && srv.qresync && !self.qresync => { - self.qresync = true; - enabled.push(x.clone()); + CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => { + self.utf8kind = Some(kind.clone()); + enabled.push(cap.clone()); } _ => (), } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index fbf29f9..ee7c8f3 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -3,14 +3,15 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; 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::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; 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::flow; use crate::imap::mailbox_view::MailboxView; @@ -24,6 +25,7 @@ use crate::mail::IMF; pub struct AuthenticatedContext<'a> { pub req: &'a Command<'static>, pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, pub user: &'a Arc, } @@ -65,6 +67,11 @@ pub async fn dispatch<'a>( message, } => ctx.append(mailbox, flags, date, message).await, + // rfc5161 ENABLE + CommandBody::Enable { capabilities } => { + ctx.enable(capabilities) + }, + // Collect other commands _ => anystate::wrong_state(ctx.req.tag.clone()), } @@ -511,6 +518,20 @@ impl<'a> AuthenticatedContext<'a> { } } + fn enable(self, cap_enable: &NonEmptyVec>) -> 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( self, mailbox: &MailboxCodec<'a>, diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index bddd0f9..0d688c0 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,7 +7,7 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; 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::flow; use crate::imap::mailbox_view::MailboxView; @@ -19,6 +19,7 @@ pub struct ExaminedContext<'a> { pub user: &'a Arc, pub mailbox: &'a mut MailboxView, pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, } 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 { req: ctx.req, server_capabilities: ctx.server_capabilities, + client_capabilities: ctx.client_capabilities, user: ctx.user, }) .await diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 28ebbe8..c8cc680 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,7 +10,7 @@ use imap_codec::imap_types::response::{Code, CodeOther}; use imap_codec::imap_types::search::SearchKey; 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::flow; use crate::imap::mailbox_view::MailboxView; @@ -23,6 +23,7 @@ pub struct SelectedContext<'a> { pub user: &'a Arc, pub mailbox: &'a mut MailboxView, pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, } pub async fn dispatch<'a>( @@ -76,6 +77,7 @@ pub async fn dispatch<'a>( authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, server_capabilities: ctx.server_capabilities, + client_capabilities: ctx.client_capabilities, user: ctx.user, }) .await diff --git a/src/imap/session.rs b/src/imap/session.rs index 55026b9..6b26478 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -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::flow; use crate::imap::response::Response; @@ -9,14 +9,17 @@ use imap_codec::imap_types::command::Command; pub struct Instance { pub login_provider: ArcLoginProvider, pub server_capabilities: ServerCapability, + pub client_capabilities: ClientCapability, pub state: flow::State, } impl Instance { pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { + let client_cap = ClientCapability::new(&cap); Self { login_provider, state: flow::State::NotAuthenticated, server_capabilities: cap, + client_capabilities: client_cap, } } @@ -36,6 +39,7 @@ impl Instance { let ctx = authenticated::AuthenticatedContext { req: &cmd, server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, user, }; authenticated::dispatch(ctx).await @@ -44,6 +48,7 @@ impl Instance { let ctx = selected::SelectedContext { req: &cmd, server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, user, mailbox, }; @@ -53,6 +58,7 @@ impl Instance { let ctx = examined::ExaminedContext { req: &cmd, server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, user, mailbox, }; -- cgit v1.2.3 From 8180baae0084faf76d49e6e03f8405487723c46f Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 20:53:25 +0100 Subject: format code --- src/imap/capability.rs | 9 +++++++-- src/imap/command/authenticated.rs | 17 ++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) (limited to 'src/imap') diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 3929558..9a641a7 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,6 +1,6 @@ use imap_codec::imap_types::core::NonEmptyVec; -use imap_codec::imap_types::response::Capability; use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; +use imap_codec::imap_types::response::Capability; use std::collections::HashSet; fn capability_unselect() -> Capability<'static> { @@ -32,7 +32,12 @@ impl Default for ServerCapability { impl ServerCapability { pub fn to_vec(&self) -> NonEmptyVec> { - self.0.iter().map(|v| v.clone()).collect::>().try_into().unwrap() + self.0 + .iter() + .map(|v| v.clone()) + .collect::>() + .try_into() + .unwrap() } #[allow(dead_code)] diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index ee7c8f3..1481a80 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, NonEmptyVec}; +use imap_codec::imap_types::core::{Atom, Literal, NonEmptyVec, QuotedChar}; use imap_codec::imap_types::datetime::DateTime; +use imap_codec::imap_types::extensions::enable::CapabilityEnable; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; -use imap_codec::imap_types::extensions::enable::CapabilityEnable; use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, MailboxName}; @@ -68,9 +68,7 @@ pub async fn dispatch<'a>( } => ctx.append(mailbox, flags, date, message).await, // rfc5161 ENABLE - CommandBody::Enable { capabilities } => { - ctx.enable(capabilities) - }, + CommandBody::Enable { capabilities } => ctx.enable(capabilities), // Collect other commands _ => anystate::wrong_state(ctx.req.tag.clone()), @@ -518,16 +516,17 @@ impl<'a> AuthenticatedContext<'a> { } } - fn enable(self, cap_enable: &NonEmptyVec>) -> Result<(Response<'static>, flow::Transition)> { + fn enable( + self, + cap_enable: &NonEmptyVec>, + ) -> 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()?, + response_builder.message("ENABLE completed").ok()?, flow::Transition::None, )) } -- cgit v1.2.3 From 3a10fb9faab6232030dcd4bf55b1b410e45d2282 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 21:29:36 +0100 Subject: advertise literal support --- src/imap/capability.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'src/imap') diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 9a641a7..631c3e2 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -23,6 +23,7 @@ impl Default for ServerCapability { Self(HashSet::from([ Capability::Imap4Rev1, Capability::Move, + Capability::LiteralPlus, capability_unselect(), //capability_condstore(), //capability_qresync(), -- cgit v1.2.3