aboutsummaryrefslogtreecommitdiff
path: root/src/imap/command
diff options
context:
space:
mode:
authorQuentin <quentin@dufour.io>2024-01-19 14:04:03 +0000
committerQuentin <quentin@dufour.io>2024-01-19 14:04:03 +0000
commit0f227e44e4996e54d2e55ed5c7a07f5458c4db4f (patch)
tree7f6c4fec623d0d99d3f09e752a360ca0f806429e /src/imap/command
parent55e26d24a08519ded6a6898453dcd6db287f45c8 (diff)
parent23aa313e11f344da07143d60ce446b5f23d5f362 (diff)
downloadaerogramme-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.rs6
-rw-r--r--src/imap/command/examined.rs164
-rw-r--r--src/imap/command/mod.rs1
-rw-r--r--src/imap/command/selected.rs110
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(),
+ ),
+ }
+ }
}