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, - )) - } + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), } } // --- 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())?), - } - } -} - 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, )) }