From 771c4eac799ec3d9f1e9c41ab1fdc75c1bcb4868 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 29 Dec 2023 17:16:41 +0100 Subject: covering imap commands --- src/imap/command/anonymous.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/imap/command') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index d258bd3..6ba19cf 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -21,7 +21,10 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Tran CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, CommandBody::Login { username, password } => ctx.login(username, password).await, - _ => Ok((Response::no("Command unavailable")?, flow::Transition::None)), + cmd => { + tracing::warn!("Unknown command {:?}", cmd); + Ok((Response::no("Command unavailable")?, flow::Transition::None)) + } } } -- cgit v1.2.3 From d2c3b641fea6106d0fa2a7940abbc026e003f707 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 1 Jan 2024 09:34:13 +0100 Subject: WIP rewrite --- src/imap/command/anonymous.rs | 19 +++++++++---------- src/imap/command/authenticated.rs | 12 ++++++------ src/imap/command/examined.rs | 16 ++++++++-------- src/imap/command/selected.rs | 10 +++++----- 4 files changed, 28 insertions(+), 29 deletions(-) (limited to 'src/imap/command') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 6ba19cf..9f4563f 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,8 +1,7 @@ use anyhow::{Error, Result}; -use boitalettres::proto::{res::body::Data as Body, Request, Response}; -use imap_codec::types::command::CommandBody; -use imap_codec::types::core::AString; -use imap_codec::types::response::{Capability, Data, Status}; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::AString; +use imap_codec::imap_types::response::{Capability, Data, Status, CommandContinuationRequest}; use crate::imap::flow; use crate::login::ArcLoginProvider; @@ -11,12 +10,12 @@ use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub login_provider: Option<&'a ArcLoginProvider>, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Status, flow::Transition)> { + match &ctx.req.body { CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)), CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, @@ -31,7 +30,7 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Tran //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Response, flow::Transition)> { + async fn capability(self) -> Result<(Status, flow::Transition)> { let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities)); Ok((res, flow::Transition::None)) @@ -41,7 +40,7 @@ impl<'a> AnonymousContext<'a> { self, username: &AString, password: &AString, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Status, flow::Transition)> { let (u, p) = ( String::try_from(username.clone())?, String::try_from(password.clone())?, @@ -81,7 +80,7 @@ impl<'a> AnonymousContext<'a> { // C: 10 logout // S: * BYE Logging out // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Response, flow::Transition)> { + async fn logout(self) -> Result<(Status, flow::Transition)> { // @FIXME we should implement From> and From> in // boitalettres/src/proto/res/body.rs Ok(( diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 2deb723..fc58425 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -4,12 +4,12 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use boitalettres::proto::res::body::Data as Body; use boitalettres::proto::{Request, Response}; -use imap_codec::types::command::{CommandBody, StatusAttribute}; -use imap_codec::types::core::NonZeroBytes; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::flag::{Flag, FlagNameAttribute}; -use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::types::response::{Code, Data, StatusAttributeValue}; +use imap_codec::imap_types::command::{CommandBody, StatusAttribute}; +use imap_codec::imap_types::core::NonZeroBytes; +use imap_codec::imap_types::datetime::MyDateTime; +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, Data, StatusAttributeValue}; use crate::imap::command::anonymous; use crate::imap::flow; diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 1740b39..8037d1d 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -3,14 +3,14 @@ use std::sync::Arc; use anyhow::Result; use boitalettres::proto::Request; use boitalettres::proto::Response; -use imap_codec::types::command::{CommandBody, SearchKey}; -use imap_codec::types::core::{Charset, NonZeroBytes}; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; -use imap_codec::types::flag::Flag; -use imap_codec::types::mailbox::Mailbox as MailboxCodec; -use imap_codec::types::response::Code; -use imap_codec::types::sequence::SequenceSet; +use imap_codec::imap_types::command::{CommandBody, SearchKey}; +use imap_codec::imap_types::core::{Charset, NonZeroBytes}; +use imap_codec::imap_types::datetime::MyDateTime; +use imap_codec::imap_types::fetch_attributes::MacroOrFetchAttributes; +use imap_codec::imap_types::flag::Flag; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; +use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::sequence::SequenceSet; use crate::imap::command::authenticated; use crate::imap::flow; diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 90a00ee..6bf068c 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use anyhow::Result; use boitalettres::proto::Request; use boitalettres::proto::Response; -use imap_codec::types::command::CommandBody; -use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::types::mailbox::Mailbox as MailboxCodec; -use imap_codec::types::response::Code; -use imap_codec::types::sequence::SequenceSet; +use imap_codec::imap_types::command::CommandBody; +use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; +use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::sequence::SequenceSet; use crate::imap::command::examined; use crate::imap::flow; -- cgit v1.2.3 From e2d77defc8496c2795860c6901d752e2c8d1c4ac Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 1 Jan 2024 17:54:48 +0100 Subject: fixed anonymous + authenticated imap logic --- src/imap/command/anonymous.rs | 85 +++++---- src/imap/command/authenticated.rs | 361 +++++++++++++++++++++++++------------- 2 files changed, 290 insertions(+), 156 deletions(-) (limited to 'src/imap/command') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 9f4563f..9bbb3b7 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,9 +1,11 @@ -use anyhow::{Error, Result}; +use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::AString; -use imap_codec::imap_types::response::{Capability, Data, Status, CommandContinuationRequest}; +use imap_codec::imap_types::core::{AString, NonEmptyVec}; +use imap_codec::imap_types::response::{Capability, Data}; +use imap_codec::imap_types::secret::Secret; use crate::imap::flow; +use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -11,18 +13,30 @@ use crate::mail::user::User; pub struct AnonymousContext<'a> { pub req: &'a Command<'static>, - pub login_provider: Option<&'a ArcLoginProvider>, + pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Status, flow::Transition)> { +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { match &ctx.req.body { - CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)), + CommandBody::Noop => Ok(( + Response::ok() + .to_req(ctx.req) + .message("Noop completed.") + .build()?, + flow::Transition::None, + )), CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, CommandBody::Login { username, password } => ctx.login(username, password).await, cmd => { - tracing::warn!("Unknown command {:?}", cmd); - Ok((Response::no("Command unavailable")?, flow::Transition::None)) + tracing::warn!("Unknown command for the anonymous state {:?}", cmd); + Ok(( + Response::bad() + .to_req(ctx.req) + .message("Command unavailable") + .build()?, + flow::Transition::None, + )) } } } @@ -30,49 +44,50 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Status, flow::Transi //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Status, flow::Transition)> { - let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; - let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities)); + async fn capability(self) -> Result<(Response, flow::Transition)> { + let capabilities: NonEmptyVec = + (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let res = Response::ok() + .to_req(self.req) + .message("Server capabilities") + .data(Data::Capability(capabilities)) + .build()?; Ok((res, flow::Transition::None)) } async fn login( self, - username: &AString, - password: &AString, - ) -> Result<(Status, flow::Transition)> { + username: &AString<'a>, + password: &Secret>, + ) -> Result<(Response, flow::Transition)> { let (u, p) = ( - String::try_from(username.clone())?, - String::try_from(password.clone())?, + std::str::from_utf8(username.as_ref())?, + std::str::from_utf8(password.declassify().as_ref())?, ); tracing::info!(user = %u, "command.login"); - let login_provider = match &self.login_provider { - Some(lp) => lp, - None => { - return Ok(( - Response::no("Login command not available (already logged in)")?, - flow::Transition::None, - )) - } - }; - - let creds = match login_provider.login(&u, &p).await { + let creds = match self.login_provider.login(&u, &p).await { Err(e) => { tracing::debug!(error=%e, "authentication failed"); return Ok(( - Response::no("Authentication failed")?, + Response::no() + .to_req(self.req) + .message("Authentication failed") + .build()?, flow::Transition::None, )); } Ok(c) => c, }; - let user = User::new(u.clone(), creds).await?; + let user = User::new(u.to_string(), creds).await?; tracing::info!(username=%u, "connected"); Ok(( - Response::ok("Completed")?, + Response::ok() + .to_req(self.req) + .message("Completed") + .build()?, flow::Transition::Authenticate(user), )) } @@ -80,15 +95,9 @@ impl<'a> AnonymousContext<'a> { // C: 10 logout // S: * BYE Logging out // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Status, flow::Transition)> { + async fn logout(self) -> Result<(Response, flow::Transition)> { // @FIXME we should implement From> and From> in // boitalettres/src/proto/res/body.rs - Ok(( - Response::ok("Logout completed")?.with_body(vec![Body::Status( - Status::bye(None, "Logging out") - .map_err(|e| Error::msg(e).context("Unable to generate IMAP status"))?, - )]), - flow::Transition::Logout, - )) + Ok((Response::bye()?, flow::Transition::Logout)) } } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index fc58425..073b005 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -2,37 +2,35 @@ use std::collections::BTreeMap; use std::sync::Arc; use anyhow::{anyhow, bail, Result}; -use boitalettres::proto::res::body::Data as Body; -use boitalettres::proto::{Request, Response}; -use imap_codec::imap_types::command::{CommandBody, StatusAttribute}; -use imap_codec::imap_types::core::NonZeroBytes; -use imap_codec::imap_types::datetime::MyDateTime; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::{Atom, Literal, QuotedChar}; +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, Data, StatusAttributeValue}; +use imap_codec::imap_types::response::{Code, CodeOther, Data}; +use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; -use crate::imap::command::anonymous; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; -use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER}; +use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; +static MAILBOX_HIERARCHY_DELIMITER: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); + pub struct AuthenticatedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub user: &'a Arc, } pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { + match &ctx.req.body { CommandBody::Create { mailbox } => ctx.create(mailbox).await, CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, - CommandBody::Rename { - mailbox, - new_mailbox, - } => ctx.rename(mailbox, new_mailbox).await, + CommandBody::Rename { from, to } => ctx.rename(from, to).await, CommandBody::Lsub { reference, mailbox_wildcard, @@ -43,8 +41,8 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: } => ctx.list(reference, mailbox_wildcard, false).await, CommandBody::Status { mailbox, - attributes, - } => ctx.status(mailbox, attributes).await, + item_names, + } => ctx.status(mailbox, item_names).await, CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await, @@ -55,90 +53,161 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: date, message, } => ctx.append(mailbox, flags, date, message).await, - _ => { - let ctx = anonymous::AnonymousContext { - req: ctx.req, - login_provider: None, - }; - anonymous::dispatch(ctx).await + cmd => { + tracing::warn!("Unknown command for the authenticated state {:?}", cmd); + Ok(( + Response::bad() + .to_req(ctx.req) + .message("Command unavailable") + .build()?, + flow::Transition::None, + )) } } } // --- PRIVATE --- -impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - - if name == INBOX { - return Ok(( - Response::bad("Cannot create INBOX")?, - flow::Transition::None, - )); +/// Convert an IMAP mailbox name/identifier representation +/// to an utf-8 string that is used internally in Aerogramme +struct MailboxName<'a>(&'a MailboxCodec<'a>); +impl<'a> TryInto<&'a str> for MailboxName<'a> { + type Error = std::str::Utf8Error; + fn try_into(self) -> Result<&'a str, Self::Error> { + match self.0 { + MailboxCodec::Inbox => Ok(INBOX), + MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), } + } +} + +impl<'a> AuthenticatedContext<'a> { + async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name = match mailbox { + MailboxCodec::Inbox => { + return Ok(( + Response::bad() + .to_req(self.req) + .message("Cannot create INBOX") + .build()?, + flow::Transition::None, + )); + } + MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?, + }; match self.user.create_mailbox(&name).await { - Ok(()) => Ok((Response::ok("CREATE complete")?, flow::Transition::None)), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + Ok(()) => Ok(( + Response::ok() + .to_req(self.req) + .message("CREATE complete") + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(&e.to_string()) + .build()?, + flow::Transition::None, + )), } } - async fn delete(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; match self.user.delete_mailbox(&name).await { - Ok(()) => Ok((Response::ok("DELETE complete")?, flow::Transition::None)), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + Ok(()) => Ok(( + Response::ok() + .to_req(self.req) + .message("DELETE complete") + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, + flow::Transition::None, + )), } } async fn rename( self, - mailbox: &MailboxCodec, - new_mailbox: &MailboxCodec, + from: &MailboxCodec<'a>, + to: &MailboxCodec<'a>, ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - let new_name = String::try_from(new_mailbox.clone())?; + let name: &str = MailboxName(from).try_into()?; + let new_name: &str = MailboxName(to).try_into()?; match self.user.rename_mailbox(&name, &new_name).await { - Ok(()) => Ok((Response::ok("RENAME complete")?, flow::Transition::None)), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + Ok(()) => Ok(( + Response::ok() + .to_req(self.req) + .message("RENAME complete") + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, + flow::Transition::None, + )), } } async fn list( self, - reference: &MailboxCodec, - mailbox_wildcard: &ListMailbox, + reference: &MailboxCodec<'a>, + mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, ) -> Result<(Response, flow::Transition)> { - let reference = String::try_from(reference.clone())?; + let reference: &str = MailboxName(reference).try_into()?; if !reference.is_empty() { return Ok(( - Response::bad("References not supported")?, + Response::bad() + .to_req(self.req) + .message("References not supported") + .build()?, flow::Transition::None, )); } - let wildcard = String::try_from(mailbox_wildcard.clone())?; + // @FIXME would probably need a rewrite to better use the imap_codec library + let wildcard = match mailbox_wildcard { + ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, + ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?, + }; if wildcard.is_empty() { if is_lsub { return Ok(( - Response::ok("LSUB complete")?.with_body(vec![Data::Lsub { - items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), - mailbox: "".try_into().unwrap(), - }]), + Response::ok() + .to_req(self.req) + .message("LSUB complete") + .data(Data::Lsub { + items: vec![], + delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + mailbox: "".try_into().unwrap(), + }) + .build()?, flow::Transition::None, )); } else { return Ok(( - Response::ok("LIST complete")?.with_body(vec![Data::List { - items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), - mailbox: "".try_into().unwrap(), - }]), + Response::ok() + .to_req(self.req) + .message("LIST complete") + .data(Data::List { + items: vec![], + delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + mailbox: "".try_into().unwrap(), + }) + .build()?, flow::Transition::None, )); } @@ -147,7 +216,7 @@ impl<'a> AuthenticatedContext<'a> { let mailboxes = self.user.list_mailboxes().await?; let mut vmailboxes = BTreeMap::new(); for mb in mailboxes.iter() { - for (i, _) in mb.match_indices(MAILBOX_HIERARCHY_DELIMITER) { + for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { if i > 0 { let smb = &mb[..i]; vmailboxes.entry(smb).or_insert(false); @@ -163,9 +232,9 @@ impl<'a> AuthenticatedContext<'a> { .to_string() .try_into() .map_err(|_| anyhow!("invalid mailbox name"))?; - let mut items = vec![FlagNameAttribute::Extension( - "Subscribed".try_into().unwrap(), - )]; + let mut items = vec![FlagNameAttribute::try_from(Atom::unvalidated( + "Subscribed", + ))?]; if !*is_real { items.push(FlagNameAttribute::Noselect); } @@ -190,21 +259,31 @@ impl<'a> AuthenticatedContext<'a> { } else { "LIST completed" }; - Ok((Response::ok(msg)?.with_body(ret), flow::Transition::None)) + Ok(( + Response::ok() + .to_req(self.req) + .message(msg) + .set_data(ret) + .build()?, + flow::Transition::None, + )) } async fn status( self, - mailbox: &MailboxCodec, - attributes: &[StatusAttribute], + mailbox: &MailboxCodec<'a>, + attributes: &[StatusDataItemName], ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - let mb_opt = self.user.open_mailbox(&name).await?; + 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::no("Mailbox does not exist")?, + Response::no() + .to_req(self.req) + .message("Mailbox does not exist") + .build()?, flow::Transition::None, )) } @@ -215,54 +294,79 @@ impl<'a> AuthenticatedContext<'a> { let mut ret_attrs = vec![]; for attr in attributes.iter() { ret_attrs.push(match attr { - StatusAttribute::Messages => StatusAttributeValue::Messages(view.exists()?), - StatusAttribute::Unseen => StatusAttributeValue::Unseen(view.unseen_count() as u32), - StatusAttribute::Recent => StatusAttributeValue::Recent(view.recent()?), - StatusAttribute::UidNext => StatusAttributeValue::UidNext(view.uidnext()), - StatusAttribute::UidValidity => { - StatusAttributeValue::UidValidity(view.uidvalidity()) + StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?), + StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32), + StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?), + StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()), + StatusDataItemName::UidValidity => { + StatusDataItem::UidValidity(view.uidvalidity()) } + StatusDataItemName::Deleted => { + bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE"); + }, + StatusDataItemName::DeletedStorage => { + bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); + }, }); } - let data = vec![Body::Data(Data::Status { + let data = Data::Status { mailbox: mailbox.clone(), - attributes: ret_attrs, - })]; + items: ret_attrs.into(), + }; Ok(( - Response::ok("STATUS completed")?.with_body(data), + Response::ok() + .to_req(self.req) + .message("STATUS completed") + .data(data) + .build()?, flow::Transition::None, )) } - async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn subscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { - Ok((Response::ok("SUBSCRIBE complete")?, flow::Transition::None)) + Ok(( + Response::ok() + .to_req(self.req) + .message("SUBSCRIBE complete") + .build()?, + flow::Transition::None, + )) } else { Ok(( - Response::bad(&format!("Mailbox {} does not exist", name))?, + Response::bad() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .build()?, flow::Transition::None, )) } } - async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn unsubscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { Ok(( - Response::bad(&format!( - "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", - name - ))?, + Response::bad() + .to_req(self.req) + .message(format!( + "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", + name + )) + .build()?, flow::Transition::None, )) } else { Ok(( - Response::bad(&format!("Mailbox {} does not exist", name))?, + Response::no() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .build()?, flow::Transition::None, )) } @@ -301,15 +405,18 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, 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::no("Mailbox does not exist")?, + Response::no() + .to_req(self.req) + .message("Mailbox does not exist") + .build()?, flow::Transition::None, )) } @@ -319,22 +426,27 @@ impl<'a> AuthenticatedContext<'a> { let (mb, data) = MailboxView::new(mb).await?; Ok(( - Response::ok("Select completed")? - .with_extra_code(Code::ReadWrite) - .with_body(data), + Response::ok() + .message("Select completed") + .code(Code::ReadWrite) + .data(data) + .build()?, flow::Transition::Select(mb), )) } - async fn examine(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, 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::no("Mailbox does not exist")?, + Response::no() + .to_req(self.req) + .message("Mailbox does not exist") + .build()?, flow::Transition::None, )) } @@ -344,40 +456,53 @@ impl<'a> AuthenticatedContext<'a> { let (mb, data) = MailboxView::new(mb).await?; Ok(( - Response::ok("Examine completed")? - .with_extra_code(Code::ReadOnly) - .with_body(data), + Response::ok() + .to_req(self.req) + .message("Examine completed") + .code(Code::ReadOnly) + .data(data) + .build()?, flow::Transition::Examine(mb), )) } async fn append( self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option, - message: &NonZeroBytes, + mailbox: &MailboxCodec<'a>, + flags: &[Flag<'a>], + date: &Option, + message: &Literal<'a>, ) -> Result<(Response, flow::Transition)> { + let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { Ok((_mb, uidvalidity, uid)) => Ok(( - Response::ok("APPEND completed")?.with_extra_code(Code::Other( - "APPENDUID".try_into().unwrap(), - Some(format!("{} {}", uidvalidity, uid)), - )), + Response::ok() + .tag(append_tag) + .message("APPEND completed") + .code(Code::Other(CodeOther::unvalidated( + format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), + ))) + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .tag(append_tag) + .message(e.to_string()) + .build()?, flow::Transition::None, )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), } } pub(crate) async fn append_internal( self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option, - message: &NonZeroBytes, + mailbox: &MailboxCodec<'a>, + flags: &[Flag<'a>], + date: &Option, + message: &Literal<'a>, ) -> Result<(Arc, ImapUidvalidity, ImapUidvalidity)> { - let name = String::try_from(mailbox.clone())?; + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; let mb = match mb_opt { @@ -389,8 +514,8 @@ impl<'a> AuthenticatedContext<'a> { bail!("Cannot set date when appending message"); } - let msg = IMF::try_from(message.as_slice()) - .map_err(|_| anyhow!("Could not parse e-mail message"))?; + let msg = + IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?; let flags = flags.iter().map(|x| x.to_string()).collect::>(); // TODO: filter allowed flags? ping @Quentin @@ -422,7 +547,7 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool { && j > 0 && matches[i - 1][j] && (wildcard[j - 1] == '*' - || (wildcard[j - 1] == '%' && name[i - 1] != MAILBOX_HIERARCHY_DELIMITER))); + || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); } } -- cgit v1.2.3 From 07eea38765aecbd53e51be199094eba2871dc7ad Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 1 Jan 2024 19:25:28 +0100 Subject: ported commands --- src/imap/command/anonymous.rs | 40 ++++------- src/imap/command/anystate.rs | 49 +++++++++++++ src/imap/command/authenticated.rs | 36 +++------- src/imap/command/examined.rs | 132 +++++++++++++++++------------------ src/imap/command/mod.rs | 17 +++++ src/imap/command/selected.rs | 143 ++++++++++++++++++++++++++++++-------- 6 files changed, 268 insertions(+), 149 deletions(-) create mode 100644 src/imap/command/anystate.rs (limited to 'src/imap/command') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 9bbb3b7..42e2a87 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -4,6 +4,7 @@ use imap_codec::imap_types::core::{AString, NonEmptyVec}; use imap_codec::imap_types::response::{Capability, Data}; use imap_codec::imap_types::secret::Secret; +use crate::imap::command::anystate; use crate::imap::flow; use crate::imap::response::Response; use crate::login::ArcLoginProvider; @@ -18,26 +19,20 @@ pub struct AnonymousContext<'a> { pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { match &ctx.req.body { - CommandBody::Noop => Ok(( - Response::ok() - .to_req(ctx.req) - .message("Noop completed.") - .build()?, - flow::Transition::None, - )), - CommandBody::Capability => ctx.capability().await, - CommandBody::Logout => ctx.logout().await, + // Any State + CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to anonymous context (3 commands) CommandBody::Login { username, password } => ctx.login(username, password).await, - cmd => { - tracing::warn!("Unknown command for the anonymous state {:?}", cmd); - Ok(( - Response::bad() - .to_req(ctx.req) - .message("Command unavailable") - .build()?, - flow::Transition::None, - )) + CommandBody::Authenticate { .. } => { + anystate::not_implemented(ctx.req.tag.clone(), "authenticate") } + //StartTLS is not implemented for now, we will probably go full TLS. + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), } } @@ -91,13 +86,4 @@ impl<'a> AnonymousContext<'a> { flow::Transition::Authenticate(user), )) } - - // C: 10 logout - // S: * BYE Logging out - // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Response, flow::Transition)> { - // @FIXME we should implement From> and From> in - // boitalettres/src/proto/res/body.rs - Ok((Response::bye()?, flow::Transition::Logout)) - } } diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs new file mode 100644 index 0000000..2d10ad8 --- /dev/null +++ b/src/imap/command/anystate.rs @@ -0,0 +1,49 @@ +use anyhow::Result; +use imap_codec::imap_types::core::{NonEmptyVec, Tag}; +use imap_codec::imap_types::response::{Capability, Data}; + +use crate::imap::flow; +use crate::imap::response::Response; + +pub(crate) fn capability(tag: Tag) -> Result<(Response, flow::Transition)> { + let capabilities: NonEmptyVec = + (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let res = Response::ok() + .tag(tag) + .message("Server capabilities") + .data(Data::Capability(capabilities)) + .build()?; + + Ok((res, flow::Transition::None)) +} + +pub(crate) fn noop_nothing(tag: Tag) -> Result<(Response, flow::Transition)> { + Ok(( + Response::ok().tag(tag).message("Noop completed.").build()?, + flow::Transition::None, + )) +} + +pub(crate) fn logout() -> Result<(Response, flow::Transition)> { + Ok((Response::bye()?, flow::Transition::Logout)) +} + +pub(crate) fn not_implemented(tag: Tag, what: &str) -> Result<(Response, flow::Transition)> { + Ok(( + Response::bad() + .tag(tag) + .message(format!("Command not implemented {}", what)) + .build()?, + flow::Transition::None, + )) +} + +pub(crate) fn wrong_state(tag: Tag) -> Result<(Response, flow::Transition)> { + Ok(( + Response::bad() + .tag(tag) + .message("Command not authorized in this state") + .build()?, + flow::Transition::None, + )) +} diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 073b005..ca4ad03 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -10,13 +10,14 @@ 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::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; -use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; +use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; static MAILBOX_HIERARCHY_DELIMITER: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); @@ -28,6 +29,12 @@ pub struct AuthenticatedContext<'a> { pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { match &ctx.req.body { + // Any state + CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to this state (11 commands) CommandBody::Create { mailbox } => ctx.create(mailbox).await, CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, CommandBody::Rename { from, to } => ctx.rename(from, to).await, @@ -53,34 +60,13 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: date, message, } => ctx.append(mailbox, flags, date, message).await, - cmd => { - tracing::warn!("Unknown command for the authenticated state {:?}", cmd); - Ok(( - Response::bad() - .to_req(ctx.req) - .message("Command unavailable") - .build()?, - flow::Transition::None, - )) - } - } -} - -// --- PRIVATE --- -/// Convert an IMAP mailbox name/identifier representation -/// to an utf-8 string that is used internally in Aerogramme -struct MailboxName<'a>(&'a MailboxCodec<'a>); -impl<'a> TryInto<&'a str> for MailboxName<'a> { - type Error = std::str::Utf8Error; - fn try_into(self) -> Result<&'a str, Self::Error> { - match self.0 { - MailboxCodec::Inbox => Ok(INBOX), - MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), - } + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), } } +// --- PRIVATE --- impl<'a> AuthenticatedContext<'a> { async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { let name = match mailbox { diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 8037d1d..cab3fdd 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -1,89 +1,111 @@ use std::sync::Arc; use anyhow::Result; -use boitalettres::proto::Request; -use boitalettres::proto::Response; -use imap_codec::imap_types::command::{CommandBody, SearchKey}; -use imap_codec::imap_types::core::{Charset, NonZeroBytes}; -use imap_codec::imap_types::datetime::MyDateTime; -use imap_codec::imap_types::fetch_attributes::MacroOrFetchAttributes; -use imap_codec::imap_types::flag::Flag; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; -use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::Charset; +use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; +use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::command::authenticated; +use crate::imap::command::anystate; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::user::User; pub struct ExaminedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'a>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - // CLOSE in examined state is not the same as in selected state - // (in selected state it also does an EXPUNGE, here it doesn't) + match &ctx.req.body { + // Any State + // noop is specific to this state + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to the EXAMINE state (specialization of the SELECTED state) + // ~3 commands -> close, fetch, search + NOOP CommandBody::Close => ctx.close().await, CommandBody::Fetch { sequence_set, - attributes, + macro_or_item_names, uid, - } => ctx.fetch(sequence_set, attributes, uid).await, + } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, CommandBody::Search { charset, criteria, uid, } => ctx.search(charset, criteria, uid).await, - CommandBody::Noop => ctx.noop().await, - CommandBody::Append { - mailbox, - flags, - date, - message, - } => ctx.append(mailbox, flags, date, message).await, - _ => { - let ctx = authenticated::AuthenticatedContext { - req: ctx.req, - user: ctx.user, - }; - authenticated::dispatch(ctx).await - } + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok(( + Response::bad() + .to_req(ctx.req) + .message("Forbidden command: can't write in read-only mode (EXAMINE)") + .build()?, + flow::Transition::None, + )), + + // The command does not belong to this state + _ => anystate::wrong_state(ctx.req.tag.clone()), } } // --- PRIVATE --- 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, flow::Transition)> { - Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) + Ok(( + Response::ok() + .to_req(self.req) + .message("CLOSE completed") + .build()?, + flow::Transition::Unselect, + )) } pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrFetchAttributes, + attributes: &MacroOrMessageDataItemNames<'a>, uid: &bool, ) -> Result<(Response, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok("FETCH completed")?.with_body(resp), + Response::ok() + .to_req(self.req) + .message("FETCH completed") + .set_data(resp) + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, flow::Transition::None, )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), } } pub async fn search( self, - _charset: &Option, - _criteria: &SearchKey, + _charset: &Option>, + _criteria: &SearchKey<'a>, _uid: &bool, ) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + Ok(( + Response::bad() + .to_req(self.req) + .message("Not implemented") + .build()?, + flow::Transition::None, + )) } pub async fn noop(self) -> Result<(Response, flow::Transition)> { @@ -91,38 +113,12 @@ impl<'a> ExaminedContext<'a> { let updates = self.mailbox.update().await?; Ok(( - Response::ok("NOOP completed.")?.with_body(updates), + Response::ok() + .to_req(self.req) + .message("NOOP completed.") + .set_data(updates) + .build()?, flow::Transition::None, )) } - - async fn append( - self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option, - message: &NonZeroBytes, - ) -> Result<(Response, flow::Transition)> { - let ctx2 = authenticated::AuthenticatedContext { - req: self.req, - user: self.user, - }; - - match ctx2.append_internal(mailbox, flags, date, message).await { - Ok((mb, uidvalidity, uid)) => { - let resp = Response::ok("APPEND completed")?.with_extra_code(Code::Other( - "APPENDUID".try_into().unwrap(), - Some(format!("{} {}", uidvalidity, uid)), - )); - - if Arc::ptr_eq(&mb, &self.mailbox.mailbox) { - let data = self.mailbox.update().await?; - Ok((resp.with_body(data), flow::Transition::None)) - } else { - Ok((resp, flow::Transition::None)) - } - } - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), - } - } } diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs index 0b7e576..dc95746 100644 --- a/src/imap/command/mod.rs +++ b/src/imap/command/mod.rs @@ -1,4 +1,21 @@ pub mod anonymous; +pub mod anystate; pub mod authenticated; pub mod examined; pub mod selected; + +use crate::mail::user::INBOX; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; + +/// Convert an IMAP mailbox name/identifier representation +/// to an utf-8 string that is used internally in Aerogramme +struct MailboxName<'a>(&'a MailboxCodec<'a>); +impl<'a> TryInto<&'a str> for MailboxName<'a> { + type Error = std::str::Utf8Error; + fn try_into(self) -> Result<&'a str, Self::Error> { + match self.0 { + MailboxCodec::Inbox => Ok(INBOX), + MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), + } + } +} diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 6bf068c..148901d 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,31 +1,48 @@ use std::sync::Arc; use anyhow::Result; -use boitalettres::proto::Request; -use boitalettres::proto::Response; -use imap_codec::imap_types::command::CommandBody; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::Charset; +use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; -use imap_codec::imap_types::response::Code; +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::command::examined; +use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::user::User; pub struct SelectedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'a>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - // Only write commands here, read commands are handled in - // `examined.rs` + match &ctx.req.body { + // Any State + // noop is specific to this state + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to this state (7 commands + NOOP) CommandBody::Close => ctx.close().await, + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Fetch { + sequence_set, + macro_or_item_names, + uid, + } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, + CommandBody::Search { + charset, + criteria, + uid, + } => ctx.search(charset, criteria, uid).await, CommandBody::Expunge => ctx.expunge().await, CommandBody::Store { sequence_set, @@ -39,14 +56,9 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans mailbox, uid, } => ctx.copy(sequence_set, mailbox, uid).await, - _ => { - let ctx = examined::ExaminedContext { - req: ctx.req, - user: ctx.user, - mailbox: ctx.mailbox, - }; - examined::dispatch(ctx).await - } + + // The command does not belong to this state + _ => anystate::wrong_state(ctx.req.tag.clone()), } } @@ -56,15 +68,78 @@ impl<'a> SelectedContext<'a> { async fn close(self) -> Result<(Response, flow::Transition)> { // We expunge messages, // but we don't send the untagged EXPUNGE responses + let tag = self.req.tag.clone(); self.expunge().await?; - Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) + Ok(( + Response::ok().tag(tag).message("CLOSE completed").build()?, + flow::Transition::Unselect, + )) + } + + pub async fn fetch( + self, + sequence_set: &SequenceSet, + attributes: &MacroOrMessageDataItemNames<'a>, + uid: &bool, + ) -> Result<(Response, flow::Transition)> { + match self.mailbox.fetch(sequence_set, attributes, uid).await { + Ok(resp) => Ok(( + Response::ok() + .to_req(self.req) + .message("FETCH completed") + .set_data(resp) + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, + flow::Transition::None, + )), + } + } + + pub async fn search( + self, + _charset: &Option>, + _criteria: &SearchKey<'a>, + _uid: &bool, + ) -> Result<(Response, flow::Transition)> { + Ok(( + Response::bad() + .to_req(self.req) + .message("Not implemented") + .build()?, + flow::Transition::None, + )) + } + + pub async fn noop(self) -> Result<(Response, flow::Transition)> { + self.mailbox.mailbox.force_sync().await?; + + let updates = self.mailbox.update().await?; + Ok(( + Response::ok() + .to_req(self.req) + .message("NOOP completed.") + .set_data(updates) + .build()?, + flow::Transition::None, + )) } async fn expunge(self) -> Result<(Response, flow::Transition)> { + let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; Ok(( - Response::ok("EXPUNGE completed")?.with_body(data), + Response::ok() + .tag(tag) + .message("EXPUNGE completed") + .data(data) + .build()?, flow::Transition::None, )) } @@ -74,7 +149,7 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, kind: &StoreType, response: &StoreResponse, - flags: &[Flag], + flags: &[Flag<'a>], uid: &bool, ) -> Result<(Response, flow::Transition)> { let data = self @@ -83,7 +158,11 @@ impl<'a> SelectedContext<'a> { .await?; Ok(( - Response::ok("STORE completed")?.with_body(data), + Response::ok() + .to_req(self.req) + .message("STORE completed") + .set_data(data) + .build()?, flow::Transition::None, )) } @@ -91,18 +170,21 @@ impl<'a> SelectedContext<'a> { async fn copy( self, sequence_set: &SequenceSet, - mailbox: &MailboxCodec, + mailbox: &MailboxCodec<'a>, uid: &bool, ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + 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::no("Destination mailbox does not exist")? - .with_extra_code(Code::TryCreate), + Response::no() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .build()?, flow::Transition::None, )) } @@ -126,10 +208,13 @@ impl<'a> SelectedContext<'a> { ); Ok(( - Response::ok("COPY completed")?.with_extra_code(Code::Other( - "COPYUID".try_into().unwrap(), - Some(copyuid_str), - )), + Response::ok() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .build()?, flow::Transition::None, )) } -- cgit v1.2.3 From 9a8d4c651e5993f09f54cf7c1eacf7a4839ea9db Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 15:35:23 +0100 Subject: commands now use imap-flow --- src/imap/command/anonymous.rs | 20 ++--- src/imap/command/anystate.rs | 27 ++++--- src/imap/command/authenticated.rs | 150 ++++++++++++++++++++------------------ src/imap/command/examined.rs | 40 +++++----- src/imap/command/selected.rs | 60 +++++++-------- 5 files changed, 155 insertions(+), 142 deletions(-) (limited to 'src/imap/command') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 42e2a87..4de5fbd 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -13,11 +13,11 @@ use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Command<'static>, + pub req: &'a Command<'a>, pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), @@ -39,14 +39,14 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Tran //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Response, flow::Transition)> { + async fn capability(self) -> Result<(Response<'a>, flow::Transition)> { let capabilities: NonEmptyVec = (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; - let res = Response::ok() + let res = Response::build() .to_req(self.req) .message("Server capabilities") .data(Data::Capability(capabilities)) - .build()?; + .ok()?; Ok((res, flow::Transition::None)) } @@ -54,7 +54,7 @@ impl<'a> AnonymousContext<'a> { self, username: &AString<'a>, password: &Secret>, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let (u, p) = ( std::str::from_utf8(username.as_ref())?, std::str::from_utf8(password.declassify().as_ref())?, @@ -65,10 +65,10 @@ impl<'a> AnonymousContext<'a> { Err(e) => { tracing::debug!(error=%e, "authentication failed"); return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Authentication failed") - .build()?, + .no()?, flow::Transition::None, )); } @@ -79,10 +79,10 @@ impl<'a> AnonymousContext<'a> { tracing::info!(username=%u, "connected"); Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("Completed") - .build()?, + .ok()?, flow::Transition::Authenticate(user), )) } diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 2d10ad8..ea3bc16 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -5,45 +5,48 @@ use imap_codec::imap_types::response::{Capability, Data}; use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag) -> Result<(Response, flow::Transition)> { +pub(crate) fn capability<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { let capabilities: NonEmptyVec = (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; - let res = Response::ok() + let res = Response::build() .tag(tag) .message("Server capabilities") .data(Data::Capability(capabilities)) - .build()?; + .ok()?; Ok((res, flow::Transition::None)) } -pub(crate) fn noop_nothing(tag: Tag) -> Result<(Response, flow::Transition)> { +pub(crate) fn noop_nothing<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::ok().tag(tag).message("Noop completed.").build()?, + Response::build().tag(tag).message("Noop completed.").ok()?, flow::Transition::None, )) } -pub(crate) fn logout() -> Result<(Response, flow::Transition)> { +pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> { Ok((Response::bye()?, flow::Transition::Logout)) } -pub(crate) fn not_implemented(tag: Tag, what: &str) -> Result<(Response, flow::Transition)> { +pub(crate) fn not_implemented<'a>( + tag: Tag<'a>, + what: &str, +) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .tag(tag) .message(format!("Command not implemented {}", what)) - .build()?, + .bad()?, flow::Transition::None, )) } -pub(crate) fn wrong_state(tag: Tag) -> Result<(Response, flow::Transition)> { +pub(crate) fn wrong_state<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .tag(tag) .message("Command not authorized in this state") - .build()?, + .bad()?, flow::Transition::None, )) } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index ca4ad03..c9f9ff7 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -20,14 +20,14 @@ use crate::mail::uidindex::*; use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; -static MAILBOX_HIERARCHY_DELIMITER: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); - pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'static>, + pub req: &'a Command<'a>, pub user: &'a Arc, } -pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>( + ctx: AuthenticatedContext<'a>, +) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), @@ -68,14 +68,14 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: // --- PRIVATE --- impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name = match mailbox { MailboxCodec::Inbox => { return Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("Cannot create INBOX") - .build()?, + .bad()?, flow::Transition::None, )); } @@ -84,38 +84,38 @@ impl<'a> AuthenticatedContext<'a> { match self.user.create_mailbox(&name).await { Ok(()) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("CREATE complete") - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(&e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } } - async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; match self.user.delete_mailbox(&name).await { Ok(()) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("DELETE complete") - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -125,23 +125,23 @@ impl<'a> AuthenticatedContext<'a> { self, from: &MailboxCodec<'a>, to: &MailboxCodec<'a>, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(from).try_into()?; let new_name: &str = MailboxName(to).try_into()?; match self.user.rename_mailbox(&name, &new_name).await { Ok(()) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("RENAME complete") - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -152,14 +152,16 @@ impl<'a> AuthenticatedContext<'a> { reference: &MailboxCodec<'a>, mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { + let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); + let reference: &str = MailboxName(reference).try_into()?; if !reference.is_empty() { return Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("References not supported") - .build()?, + .bad()?, flow::Transition::None, )); } @@ -172,28 +174,28 @@ impl<'a> AuthenticatedContext<'a> { if wildcard.is_empty() { if is_lsub { return Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("LSUB complete") .data(Data::Lsub { items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox: "".try_into().unwrap(), }) - .build()?, + .ok()?, flow::Transition::None, )); } else { return Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("LIST complete") .data(Data::List { items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox: "".try_into().unwrap(), }) - .build()?, + .ok()?, flow::Transition::None, )); } @@ -227,13 +229,13 @@ impl<'a> AuthenticatedContext<'a> { if is_lsub { ret.push(Data::Lsub { items, - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox, }); } else { ret.push(Data::List { items, - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox, }); } @@ -246,11 +248,11 @@ impl<'a> AuthenticatedContext<'a> { "LIST completed" }; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message(msg) - .set_data(ret) - .build()?, + .many_data(ret) + .ok()?, flow::Transition::None, )) } @@ -259,23 +261,23 @@ impl<'a> AuthenticatedContext<'a> { self, mailbox: &MailboxCodec<'a>, attributes: &[StatusDataItemName], - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, 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::no() + Response::build() .to_req(self.req) .message("Mailbox does not exist") - .build()?, + .no()?, flow::Transition::None, )) } }; - let (view, _data) = MailboxView::new(mb).await?; + let view = MailboxView::new(mb).await; let mut ret_attrs = vec![]; for attr in attributes.iter() { @@ -302,57 +304,63 @@ impl<'a> AuthenticatedContext<'a> { }; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("STATUS completed") .data(data) - .build()?, + .ok()?, flow::Transition::None, )) } - async fn subscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn subscribe( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("SUBSCRIBE complete") - .build()?, + .ok()?, flow::Transition::None, )) } else { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message(format!("Mailbox {} does not exist", name)) - .build()?, + .bad()?, flow::Transition::None, )) } } - async fn unsubscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn unsubscribe( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message(format!( "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", name )) - .build()?, + .bad()?, flow::Transition::None, )) } else { Ok(( - Response::no() + Response::build() .to_req(self.req) .message(format!("Mailbox {} does not exist", name)) - .build()?, + .no()?, flow::Transition::None, )) } @@ -391,7 +399,7 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -399,29 +407,30 @@ impl<'a> AuthenticatedContext<'a> { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Mailbox does not exist") - .build()?, + .no()?, flow::Transition::None, )) } }; tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); - let (mb, data) = MailboxView::new(mb).await?; + let mb = MailboxView::new(mb).await; + let data = mb.summary()?; Ok(( - Response::ok() + Response::build() .message("Select completed") .code(Code::ReadWrite) - .data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::Select(mb), )) } - async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -429,25 +438,26 @@ impl<'a> AuthenticatedContext<'a> { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Mailbox does not exist") - .build()?, + .no()?, flow::Transition::None, )) } }; tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); - let (mb, data) = MailboxView::new(mb).await?; + let mb = MailboxView::new(mb).await; + let data = mb.summary()?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("Examine completed") .code(Code::ReadOnly) - .data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::Examine(mb), )) } @@ -458,24 +468,24 @@ impl<'a> AuthenticatedContext<'a> { flags: &[Flag<'a>], date: &Option, message: &Literal<'a>, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { Ok((_mb, uidvalidity, uid)) => Ok(( - Response::ok() + Response::build() .tag(append_tag) .message("APPEND completed") .code(Code::Other(CodeOther::unvalidated( format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), ))) - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .tag(append_tag) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index cab3fdd..7f9c39c 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -19,7 +19,7 @@ pub struct ExaminedContext<'a> { pub mailbox: &'a mut MailboxView, } -pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state @@ -41,10 +41,10 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans } => ctx.search(charset, criteria, uid).await, CommandBody::Noop | CommandBody::Check => ctx.noop().await, CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok(( - Response::bad() + Response::build() .to_req(ctx.req) .message("Forbidden command: can't write in read-only mode (EXAMINE)") - .build()?, + .bad()?, flow::Transition::None, )), @@ -58,12 +58,12 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans 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, flow::Transition)> { + async fn close(self) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("CLOSE completed") - .build()?, + .ok()?, flow::Transition::Unselect, )) } @@ -71,23 +71,23 @@ impl<'a> ExaminedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("FETCH completed") - .set_data(resp) - .build()?, + .set_body(resp) + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -98,26 +98,26 @@ impl<'a> ExaminedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("Not implemented") - .build()?, + .bad()?, flow::Transition::None, )) } - pub async fn noop(self) -> Result<(Response, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("NOOP completed.") - .set_data(updates) - .build()?, + .set_body(updates) + .ok()?, flow::Transition::None, )) } diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 148901d..cd5d221 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -23,7 +23,7 @@ pub struct SelectedContext<'a> { pub mailbox: &'a mut MailboxView, } -pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state @@ -65,13 +65,13 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans // --- PRIVATE --- impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response, flow::Transition)> { + async fn close(self) -> Result<(Response<'a>, flow::Transition)> { // We expunge messages, // but we don't send the untagged EXPUNGE responses let tag = self.req.tag.clone(); self.expunge().await?; Ok(( - Response::ok().tag(tag).message("CLOSE completed").build()?, + Response::build().tag(tag).message("CLOSE completed").ok()?, flow::Transition::Unselect, )) } @@ -79,23 +79,23 @@ impl<'a> SelectedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("FETCH completed") - .set_data(resp) - .build()?, + .set_body(resp) + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -106,40 +106,40 @@ impl<'a> SelectedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("Not implemented") - .build()?, + .bad()?, flow::Transition::None, )) } - pub async fn noop(self) -> Result<(Response, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("NOOP completed.") - .set_data(updates) - .build()?, + .set_body(updates) + .ok()?, flow::Transition::None, )) } - async fn expunge(self) -> Result<(Response, flow::Transition)> { + async fn expunge(self) -> Result<(Response<'a>, flow::Transition)> { let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; Ok(( - Response::ok() + Response::build() .tag(tag) .message("EXPUNGE completed") - .data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::None, )) } @@ -151,18 +151,18 @@ impl<'a> SelectedContext<'a> { response: &StoreResponse, flags: &[Flag<'a>], uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let data = self .mailbox .store(sequence_set, kind, response, flags, uid) .await?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("STORE completed") - .set_data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::None, )) } @@ -172,7 +172,7 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, mailbox: &MailboxCodec<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -180,11 +180,11 @@ impl<'a> SelectedContext<'a> { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Destination mailbox does not exist") .code(Code::TryCreate) - .build()?, + .no()?, flow::Transition::None, )) } @@ -208,13 +208,13 @@ impl<'a> SelectedContext<'a> { ); Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("COPY completed") .code(Code::Other(CodeOther::unvalidated( format!("COPYUID {}", copyuid_str).into_bytes(), ))) - .build()?, + .ok()?, flow::Transition::None, )) } -- cgit v1.2.3 From 0d667a30301bec47c03314ff0e449a220ad3b913 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 20:23:33 +0100 Subject: compile with imap-flow --- src/imap/command/anonymous.rs | 22 +++++---------------- src/imap/command/anystate.rs | 6 +++--- src/imap/command/authenticated.rs | 40 +++++++++++++++++++++++++-------------- src/imap/command/examined.rs | 16 ++++++++-------- src/imap/command/selected.rs | 24 ++++++++++++----------- 5 files changed, 55 insertions(+), 53 deletions(-) (limited to 'src/imap/command') diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 4de5fbd..fbd10e9 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,7 +1,6 @@ use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::{AString, NonEmptyVec}; -use imap_codec::imap_types::response::{Capability, Data}; +use imap_codec::imap_types::core::AString; use imap_codec::imap_types::secret::Secret; use crate::imap::command::anystate; @@ -13,16 +12,16 @@ use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), - CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) CommandBody::Login { username, password } => ctx.login(username, password).await, @@ -39,22 +38,11 @@ pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response<'a>, fl //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Response<'a>, flow::Transition)> { - let capabilities: NonEmptyVec = - (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; - let res = Response::build() - .to_req(self.req) - .message("Server capabilities") - .data(Data::Capability(capabilities)) - .ok()?; - Ok((res, flow::Transition::None)) - } - async fn login( self, username: &AString<'a>, password: &Secret>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let (u, p) = ( std::str::from_utf8(username.as_ref())?, std::str::from_utf8(password.declassify().as_ref())?, diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index ea3bc16..42fe645 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -5,7 +5,7 @@ use imap_codec::imap_types::response::{Capability, Data}; use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { let capabilities: NonEmptyVec = (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; let res = Response::build() @@ -17,7 +17,7 @@ pub(crate) fn capability<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transi Ok((res, flow::Transition::None)) } -pub(crate) fn noop_nothing<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build().tag(tag).message("Noop completed.").ok()?, flow::Transition::None, @@ -41,7 +41,7 @@ pub(crate) fn not_implemented<'a>( )) } -pub(crate) fn wrong_state<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .tag(tag) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index c9f9ff7..74ebbfa 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -21,18 +21,18 @@ use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW} use crate::mail::IMF; pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub user: &'a Arc, } pub async fn dispatch<'a>( ctx: AuthenticatedContext<'a>, -) -> Result<(Response<'a>, flow::Transition)> { +) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), - CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) CommandBody::Create { mailbox } => ctx.create(mailbox).await, @@ -68,7 +68,10 @@ pub async fn dispatch<'a>( // --- PRIVATE --- impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn create( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name = match mailbox { MailboxCodec::Inbox => { return Ok(( @@ -100,7 +103,10 @@ impl<'a> AuthenticatedContext<'a> { } } - async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn delete( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; match self.user.delete_mailbox(&name).await { @@ -125,7 +131,7 @@ impl<'a> AuthenticatedContext<'a> { self, from: &MailboxCodec<'a>, to: &MailboxCodec<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(from).try_into()?; let new_name: &str = MailboxName(to).try_into()?; @@ -152,7 +158,7 @@ impl<'a> AuthenticatedContext<'a> { reference: &MailboxCodec<'a>, mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); let reference: &str = MailboxName(reference).try_into()?; @@ -259,9 +265,9 @@ impl<'a> AuthenticatedContext<'a> { async fn status( self, - mailbox: &MailboxCodec<'a>, + mailbox: &MailboxCodec<'static>, attributes: &[StatusDataItemName], - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> 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 { @@ -316,7 +322,7 @@ impl<'a> AuthenticatedContext<'a> { async fn subscribe( self, mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { @@ -341,7 +347,7 @@ impl<'a> AuthenticatedContext<'a> { async fn unsubscribe( self, mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { @@ -399,7 +405,10 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn select( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -430,7 +439,10 @@ impl<'a> AuthenticatedContext<'a> { )) } - async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn examine( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -468,7 +480,7 @@ impl<'a> AuthenticatedContext<'a> { flags: &[Flag<'a>], date: &Option, message: &Literal<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { Ok((_mb, uidvalidity, uid)) => Ok(( diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7f9c39c..eec85cd 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -14,17 +14,17 @@ use crate::imap::response::Response; use crate::mail::user::User; pub struct ExaminedContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } -pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { +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::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to the EXAMINE state (specialization of the SELECTED state) // ~3 commands -> close, fetch, search + NOOP @@ -58,7 +58,7 @@ pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response<'a>, flo 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<'a>, flow::Transition)> { + async fn close(self) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) @@ -71,9 +71,9 @@ impl<'a> ExaminedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'static>, uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( Response::build() @@ -98,7 +98,7 @@ impl<'a> ExaminedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) @@ -108,7 +108,7 @@ impl<'a> ExaminedContext<'a> { )) } - pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index cd5d221..d5dcd61 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -18,17 +18,19 @@ use crate::imap::response::Response; use crate::mail::user::User; pub struct SelectedContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } -pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub async fn dispatch<'a>( + ctx: SelectedContext<'a>, +) -> 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::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) CommandBody::Close => ctx.close().await, @@ -65,7 +67,7 @@ pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response<'a>, flo // --- PRIVATE --- impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response<'a>, flow::Transition)> { + async fn close(self) -> Result<(Response<'static>, flow::Transition)> { // We expunge messages, // but we don't send the untagged EXPUNGE responses let tag = self.req.tag.clone(); @@ -79,9 +81,9 @@ impl<'a> SelectedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'static>, uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( Response::build() @@ -106,7 +108,7 @@ impl<'a> SelectedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) @@ -116,7 +118,7 @@ impl<'a> SelectedContext<'a> { )) } - pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; @@ -130,7 +132,7 @@ impl<'a> SelectedContext<'a> { )) } - async fn expunge(self) -> Result<(Response<'a>, flow::Transition)> { + async fn expunge(self) -> Result<(Response<'static>, flow::Transition)> { let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; @@ -151,7 +153,7 @@ impl<'a> SelectedContext<'a> { response: &StoreResponse, flags: &[Flag<'a>], uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let data = self .mailbox .store(sequence_set, kind, response, flags, uid) @@ -172,7 +174,7 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, mailbox: &MailboxCodec<'a>, uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; -- cgit v1.2.3 From 0cc13f891cdcdc474416cdb63d48245a1820af10 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 22:32:02 +0100 Subject: migration to imap-flow seems done! --- src/imap/command/authenticated.rs | 1 + src/imap/command/examined.rs | 12 +++++++++--- src/imap/command/selected.rs | 12 +++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) (limited to 'src/imap/command') diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 74ebbfa..1bb4c6d 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -432,6 +432,7 @@ impl<'a> AuthenticatedContext<'a> { Ok(( Response::build() .message("Select completed") + .to_req(self.req) .code(Code::ReadWrite) .set_body(data) .ok()?, diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index eec85cd..7de94f4 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::command::anystate; +use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; @@ -48,8 +48,14 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl flow::Transition::None, )), - // The command does not belong to this state - _ => anystate::wrong_state(ctx.req.tag.clone()), + // In examined mode, we fallback to authenticated when needed + _ => { + authenticated::dispatch(authenticated::AuthenticatedContext { + req: ctx.req, + user: ctx.user, + }) + .await + } } } diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index d5dcd61..220a952 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::command::{anystate, MailboxName}; +use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; @@ -59,8 +59,14 @@ pub async fn dispatch<'a>( uid, } => ctx.copy(sequence_set, mailbox, uid).await, - // The command does not belong to this state - _ => anystate::wrong_state(ctx.req.tag.clone()), + // In selected mode, we fallback to authenticated when needed + _ => { + authenticated::dispatch(authenticated::AuthenticatedContext { + req: ctx.req, + user: ctx.user, + }) + .await + } } } -- cgit v1.2.3