diff options
author | Quentin <quentin@dufour.io> | 2024-01-19 14:04:03 +0000 |
---|---|---|
committer | Quentin <quentin@dufour.io> | 2024-01-19 14:04:03 +0000 |
commit | 0f227e44e4996e54d2e55ed5c7a07f5458c4db4f (patch) | |
tree | 7f6c4fec623d0d99d3f09e752a360ca0f806429e /src/imap/command | |
parent | 55e26d24a08519ded6a6898453dcd6db287f45c8 (diff) | |
parent | 23aa313e11f344da07143d60ce446b5f23d5f362 (diff) | |
download | aerogramme-0f227e44e4996e54d2e55ed5c7a07f5458c4db4f.tar.gz aerogramme-0f227e44e4996e54d2e55ed5c7a07f5458c4db4f.zip |
Merge pull request 'Implement IDLE' (#72) from feat/idle into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/pulls/72
Diffstat (limited to 'src/imap/command')
-rw-r--r-- | src/imap/command/authenticated.rs | 6 | ||||
-rw-r--r-- | src/imap/command/examined.rs | 164 | ||||
-rw-r--r-- | src/imap/command/mod.rs | 1 | ||||
-rw-r--r-- | src/imap/command/selected.rs | 110 |
4 files changed, 93 insertions, 188 deletions
diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 9b6bb24..3fd132f 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -405,7 +405,7 @@ impl<'a> AuthenticatedContext<'a> { it is therefore correct to not return it even if there are unseen messages RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. - + 20 select "INBOX.achats" * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) @@ -453,7 +453,7 @@ impl<'a> AuthenticatedContext<'a> { .code(Code::ReadWrite) .set_body(data) .ok()?, - flow::Transition::Select(mb), + flow::Transition::Select(mb, flow::MailboxPerm::ReadWrite), )) } @@ -491,7 +491,7 @@ impl<'a> AuthenticatedContext<'a> { .code(Code::ReadOnly) .set_body(data) .ok()?, - flow::Transition::Examine(mb), + flow::Transition::Select(mb, flow::MailboxPerm::ReadOnly), )) } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs deleted file mode 100644 index 9fc0990..0000000 --- a/src/imap/command/examined.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::sync::Arc; -use std::num::NonZeroU64; - -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier}; -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::attributes::AttributesProxy; -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anystate, authenticated}; -use crate::imap::flow; -use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; -use crate::imap::response::Response; -use crate::mail::user::User; - -pub struct ExaminedContext<'a> { - pub req: &'a Command<'static>, - pub user: &'a Arc<User>, - pub mailbox: &'a mut MailboxView, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, -} - -pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - // noop is specific to this state - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to the EXAMINE state (specialization of the SELECTED state) - // ~3 commands -> close, fetch, search + NOOP - CommandBody::Close => ctx.close("CLOSE").await, - CommandBody::Fetch { - sequence_set, - macro_or_item_names, - modifiers, - uid, - } => ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid).await, - CommandBody::Search { - charset, - criteria, - uid, - } => ctx.search(charset, criteria, uid).await, - CommandBody::Noop | CommandBody::Check => ctx.noop().await, - CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok(( - Response::build() - .to_req(ctx.req) - .message("Forbidden command: can't write in read-only mode (EXAMINE)") - .no()?, - flow::Transition::None, - )), - - // UNSELECT extension (rfc3691) - CommandBody::Unselect => ctx.close("UNSELECT").await, - - // In examined mode, we fallback to authenticated when needed - _ => { - authenticated::dispatch(authenticated::AuthenticatedContext { - req: ctx.req, - server_capabilities: ctx.server_capabilities, - client_capabilities: ctx.client_capabilities, - user: ctx.user, - }) - .await - } - } -} - -// --- 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, kind: &str) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("{} completed", kind)) - .ok()?, - flow::Transition::Unselect, - )) - } - - pub async fn fetch( - self, - sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let ap = AttributesProxy::new(attributes, modifiers, *uid); - let mut changed_since: Option<NonZeroU64> = None; - modifiers.iter().for_each(|m| match m { - FetchModifier::ChangedSince(val) => { - changed_since = Some(*val); - }, - }); - - match self.mailbox.fetch(sequence_set, &ap, changed_since, uid).await { - Ok(resp) => { - // Capabilities enabling logic only on successful command - // (according to my understanding of the spec) - self.client_capabilities.attributes_enable(&ap); - self.client_capabilities.fetch_modifiers_enable(modifiers); - - Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )) - }, - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - pub async fn search( - self, - charset: &Option<Charset<'a>>, - criteria: &SearchKey<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; - if enable_condstore { - self.client_capabilities.enable_condstore(); - } - Ok(( - Response::build() - .to_req(self.req) - .set_body(found) - .message("SEARCH completed") - .ok()?, - flow::Transition::None, - )) - } - - pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { - self.mailbox.internal.mailbox.force_sync().await?; - - let updates = self.mailbox.update(UpdateParameters::default()).await?; - Ok(( - Response::build() - .to_req(self.req) - .message("NOOP completed.") - .set_body(updates) - .ok()?, - flow::Transition::None, - )) - } -} diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs index dc95746..073040e 100644 --- a/src/imap/command/mod.rs +++ b/src/imap/command/mod.rs @@ -1,7 +1,6 @@ pub mod anonymous; pub mod anystate; pub mod authenticated; -pub mod examined; pub mod selected; use crate::mail::user::INBOX; diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index c13b71a..98b3b00 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use std::num::NonZeroU64; +use std::sync::Arc; use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier}; @@ -11,12 +11,12 @@ 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::attributes::AttributesProxy; use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; use crate::imap::response::Response; -use crate::imap::attributes::AttributesProxy; use crate::mail::user::User; pub struct SelectedContext<'a> { @@ -25,6 +25,7 @@ pub struct SelectedContext<'a> { pub mailbox: &'a mut MailboxView, pub server_capabilities: &'a ServerCapability, pub client_capabilities: &'a mut ClientCapability, + pub perm: &'a flow::MailboxPerm, } pub async fn dispatch<'a>( @@ -39,14 +40,20 @@ pub async fn dispatch<'a>( CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) - CommandBody::Close => ctx.close().await, + CommandBody::Close => match ctx.perm { + flow::MailboxPerm::ReadWrite => ctx.close().await, + flow::MailboxPerm::ReadOnly => ctx.examine_close().await, + }, CommandBody::Noop | CommandBody::Check => ctx.noop().await, CommandBody::Fetch { sequence_set, macro_or_item_names, modifiers, uid, - } => ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid).await, + } => { + ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid) + .await + } CommandBody::Search { charset, criteria, @@ -60,7 +67,10 @@ pub async fn dispatch<'a>( flags, modifiers, uid, - } => ctx.store(sequence_set, kind, response, flags, modifiers, uid).await, + } => { + ctx.store(sequence_set, kind, response, flags, modifiers, uid) + .await + } CommandBody::Copy { sequence_set, mailbox, @@ -75,6 +85,15 @@ pub async fn dispatch<'a>( // UNSELECT extension (rfc3691) CommandBody::Unselect => ctx.unselect().await, + // IDLE extension (rfc2177) + CommandBody::Idle => Ok(( + Response::build() + .to_req(ctx.req) + .message("DUMMY command due to anti-pattern in the code") + .ok()?, + flow::Transition::Idle(ctx.req.tag.clone(), tokio::sync::Notify::new()), + )), + // In selected mode, we fallback to authenticated when needed _ => { authenticated::dispatch(authenticated::AuthenticatedContext { @@ -102,6 +121,18 @@ impl<'a> SelectedContext<'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 examine_close(self) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("CLOSE completed") + .ok()?, + flow::Transition::Unselect, + )) + } + async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() @@ -124,10 +155,14 @@ impl<'a> SelectedContext<'a> { modifiers.iter().for_each(|m| match m { FetchModifier::ChangedSince(val) => { changed_since = Some(*val); - }, + } }); - match self.mailbox.fetch(sequence_set, &ap, changed_since, uid).await { + match self + .mailbox + .fetch(sequence_set, &ap, changed_since, uid) + .await + { Ok(resp) => { // Capabilities enabling logic only on successful command // (according to my understanding of the spec) @@ -143,7 +178,7 @@ impl<'a> SelectedContext<'a> { .ok()?, flow::Transition::None, )) - }, + } Err(e) => Ok(( Response::build() .to_req(self.req) @@ -189,6 +224,10 @@ impl<'a> SelectedContext<'a> { } async fn expunge(self) -> Result<(Response<'static>, flow::Transition)> { + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; @@ -211,11 +250,15 @@ impl<'a> SelectedContext<'a> { modifiers: &[StoreModifier], uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + let mut unchanged_since: Option<NonZeroU64> = None; modifiers.iter().for_each(|m| match m { StoreModifier::UnchangedSince(val) => { unchanged_since = Some(*val); - }, + } }); let (data, modified) = self @@ -224,25 +267,30 @@ impl<'a> SelectedContext<'a> { .await?; let mut ok_resp = Response::build() - .to_req(self.req) - .message("STORE completed") - .set_body(data); - + .to_req(self.req) + .message("STORE completed") + .set_body(data); match modified[..] { [] => (), [_head, ..] => { - let modified_str = format!("MODIFIED {}", modified.into_iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",")); - ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated(modified_str.into_bytes()))); - }, + let modified_str = format!( + "MODIFIED {}", + modified + .into_iter() + .map(|x| x.to_string()) + .collect::<Vec<_>>() + .join(",") + ); + ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated( + modified_str.into_bytes(), + ))); + } }; - self.client_capabilities.store_modifiers_enable(modifiers); - Ok((ok_resp.ok()?, - flow::Transition::None, - )) + Ok((ok_resp.ok()?, flow::Transition::None)) } async fn copy( @@ -251,6 +299,11 @@ impl<'a> SelectedContext<'a> { mailbox: &MailboxCodec<'a>, uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { + //@FIXME Could copy be valid in EXAMINE mode? + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -303,6 +356,10 @@ impl<'a> SelectedContext<'a> { mailbox: &MailboxCodec<'a>, uid: &bool, ) -> Result<(Response<'static>, flow::Transition)> { + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -350,4 +407,17 @@ impl<'a> SelectedContext<'a> { flow::Transition::None, )) } + + fn fail_read_only(&self) -> Option<Response<'static>> { + match self.perm { + flow::MailboxPerm::ReadWrite => None, + flow::MailboxPerm::ReadOnly => Some( + Response::build() + .to_req(self.req) + .message("Write command are forbidden while exmining mailbox") + .no() + .unwrap(), + ), + } + } } |