From 497ad4b5eae7a2ddf3d7a945313c478d23414249 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 30 Jun 2022 11:28:03 +0200 Subject: [PATCH] Split out Examined state and add prototypes for IMAP command handlers --- src/imap/command/authenticated.rs | 53 ++++++++++++++++++- src/imap/command/examined.rs | 88 +++++++++++++++++++++++++++++++ src/imap/command/mod.rs | 1 + src/imap/command/selected.rs | 74 +++++++++++++++++--------- src/imap/flow.rs | 5 ++ src/imap/mailbox_view.rs | 10 +++- src/imap/session.rs | 10 +++- 7 files changed, 212 insertions(+), 29 deletions(-) create mode 100644 src/imap/command/examined.rs diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 8e8d969..391b7ff 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -1,6 +1,6 @@ use anyhow::Result; use boitalettres::proto::{Request, Response}; -use imap_codec::types::command::CommandBody; +use imap_codec::types::command::{CommandBody, StatusAttribute}; use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::types::response::Code; @@ -17,6 +17,9 @@ pub struct AuthenticatedContext<'a> { pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, flow::Transition)> { match &ctx.req.command.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::Lsub { reference, mailbox_wildcard, @@ -25,7 +28,10 @@ pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, fl reference, mailbox_wildcard, } => ctx.list(reference, mailbox_wildcard).await, + CommandBody::Status { mailbox, attributes } => + ctx.status(mailbox, attributes).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await, + CommandBody::Examine { mailbox } => ctx.examine(mailbox).await, _ => { let ctx = anonymous::AnonymousContext { req: ctx.req, @@ -39,6 +45,18 @@ pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, fl // --- PRIVATE --- impl<'a> AuthenticatedContext<'a> { + async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + + async fn delete(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + + async fn rename(self, mailbox: &MailboxCodec, new_mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + async fn lsub( self, _reference: &MailboxCodec, @@ -55,6 +73,14 @@ impl<'a> AuthenticatedContext<'a> { Ok((Response::bad("Not implemented")?, flow::Transition::None)) } + async fn status( + self, + mailbox: &MailboxCodec, + attributes: &[StatusAttribute], + ) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + /* * TRACE BEGIN --- @@ -107,4 +133,29 @@ impl<'a> AuthenticatedContext<'a> { flow::Transition::Select(mb), )) } + + async fn examine(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { + let name = String::try_from(mailbox.clone())?; + + 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")?, + flow::Transition::None, + )) + } + }; + tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); + + let (mb, data) = MailboxView::new(mb).await?; + + Ok(( + Response::ok("Examine completed")? + .with_extra_code(Code::ReadOnly) + .with_body(data), + flow::Transition::Examine(mb), + )) + } } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs new file mode 100644 index 0000000..d459cf0 --- /dev/null +++ b/src/imap/command/examined.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use boitalettres::proto::Request; +use boitalettres::proto::Response; +use imap_codec::types::command::{CommandBody, SearchKey}; +use imap_codec::types::core::Charset; +use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; + +use imap_codec::types::sequence::SequenceSet; + +use crate::imap::command::authenticated; +use crate::imap::flow; +use crate::imap::mailbox_view::MailboxView; + +use crate::mail::user::User; + +pub struct ExaminedContext<'a> { + pub req: &'a Request, + pub user: &'a User, + pub mailbox: &'a mut MailboxView, +} + +pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> 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) + CommandBody::Close => ctx.close().await, + CommandBody::Fetch { + sequence_set, + attributes, + uid, + } => ctx.fetch(sequence_set, attributes, uid).await, + CommandBody::Search { + charset, + criteria, + uid, + } => ctx.search(charset, criteria, uid).await, + CommandBody::Noop => ctx.noop().await, + _ => { + let ctx = authenticated::AuthenticatedContext { + req: ctx.req, + user: ctx.user, + }; + authenticated::dispatch(ctx).await + } + } +} + +// --- PRIVATE --- + +impl<'a> ExaminedContext<'a> { + async fn close( + self, + ) -> Result<(Response, flow::Transition)> { + Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) + } + + pub async fn fetch( + self, + sequence_set: &SequenceSet, + attributes: &MacroOrFetchAttributes, + 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), + flow::Transition::None, + )), + Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + } + } + + pub async fn search( + self, + charset: &Option, + criteria: &SearchKey, + uid: &bool, + ) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + + pub async fn noop(self) -> Result<(Response, flow::Transition)> { + let updates = self.mailbox.sync_update().await?; + Ok(( + Response::ok("NOOP completed.")?.with_body(updates), + flow::Transition::None, + )) + } +} diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs index c4fa4d8..559dddf 100644 --- a/src/imap/command/mod.rs +++ b/src/imap/command/mod.rs @@ -1,3 +1,4 @@ pub mod anonymous; pub mod authenticated; pub mod selected; +pub mod examined; diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index b3a2ffd..b8598f5 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -2,12 +2,13 @@ use anyhow::Result; use boitalettres::proto::Request; use boitalettres::proto::Response; use imap_codec::types::command::CommandBody; - +use imap_codec::types::mailbox::{Mailbox as MailboxCodec}; +use imap_codec::types::flag::{Flag, StoreType, StoreResponse}; use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; use imap_codec::types::sequence::SequenceSet; -use crate::imap::command::authenticated; +use crate::imap::command::examined; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; @@ -21,18 +22,29 @@ pub struct SelectedContext<'a> { pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response, flow::Transition)> { match &ctx.req.command.body { - CommandBody::Noop => ctx.noop().await, - CommandBody::Fetch { + // Only write commands here, read commands are handled in + // `examined.rs` + CommandBody::Close => ctx.close().await, + CommandBody::Expunge => ctx.expunge().await, + CommandBody::Store { + sequence_set, + kind, + response, + flags, + uid + } => ctx.store(sequence_set, kind, response, flags, uid).await, + CommandBody::Copy { sequence_set, - attributes, + mailbox, uid, - } => ctx.fetch(sequence_set, attributes, uid).await, + } => ctx.copy(sequence_set, mailbox, uid).await, _ => { - let ctx = authenticated::AuthenticatedContext { + let ctx = examined::ExaminedContext { req: ctx.req, user: ctx.user, + mailbox: ctx.mailbox, }; - authenticated::dispatch(ctx).await + examined::dispatch(ctx).await } } } @@ -40,26 +52,38 @@ pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response, flow::T // --- PRIVATE --- impl<'a> SelectedContext<'a> { - pub async fn fetch( + async fn close( self, - sequence_set: &SequenceSet, - attributes: &MacroOrFetchAttributes, - 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), - flow::Transition::None, - )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), - } + // We expunge messages, + // but we don't send the untagged EXPUNGE responses + self.expunge().await?; + Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) } - pub async fn noop(self) -> Result<(Response, flow::Transition)> { - let updates = self.mailbox.update().await?; - Ok(( - Response::ok("NOOP completed.")?.with_body(updates), - flow::Transition::None, - )) + async fn expunge( + self, + ) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + + async fn store( + self, + sequence_set: &SequenceSet, + kind: &StoreType, + response: &StoreResponse, + flags: &[Flag], + uid: &bool, + ) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + + async fn copy( + self, + sequence_set: &SequenceSet, + mailbox: &MailboxCodec, + uid: &bool, + ) -> Result<(Response, flow::Transition)> { + Ok((Response::bad("Not implemented")?, flow::Transition::None)) } } diff --git a/src/imap/flow.rs b/src/imap/flow.rs index e1ea99f..feb78ac 100644 --- a/src/imap/flow.rs +++ b/src/imap/flow.rs @@ -19,12 +19,15 @@ pub enum State { NotAuthenticated, Authenticated(User), Selected(User, MailboxView), + // Examined is like Selected, but indicates that the mailbox is read-only + Examined(User, MailboxView), Logout, } pub enum Transition { None, Authenticate(User), + Examine(MailboxView), Select(MailboxView), Unselect, Logout, @@ -38,7 +41,9 @@ impl State { (s, Transition::None) => Ok(s), (State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)), (State::Authenticated(u), Transition::Select(m)) => Ok(State::Selected(u, m)), + (State::Authenticated(u), Transition::Examine(m)) => Ok(State::Examined(u, m)), (State::Selected(u, _), Transition::Unselect) => Ok(State::Authenticated(u)), + (State::Examined(u, _), Transition::Unselect) => Ok(State::Authenticated(u)), (_, Transition::Logout) => Ok(State::Logout), _ => Err(Error::ForbiddenTransition), } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index e1ea516..58e71a2 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -66,12 +66,18 @@ impl MailboxView { } /// Looks up state changes in the mailbox and produces a set of IMAP - /// responses describing the new state. - pub async fn update(&mut self) -> Result> { + /// responses describing the changes. + pub async fn sync_update(&mut self) -> Result> { self.mailbox.sync().await?; // TODO THIS IS JUST A TEST REMOVE LATER self.mailbox.test().await?; + self.update().await + } + + /// Produces a set of IMAP responses describing the change between + /// what the client knows and what is actually in the mailbox. + pub async fn update(&mut self) -> Result> { let new_view = MailboxView { mailbox: self.mailbox.clone(), known_state: self.mailbox.current_uid_index().await, diff --git a/src/imap/session.rs b/src/imap/session.rs index b7e2059..17753ea 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -7,7 +7,7 @@ use futures::future::FutureExt; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::{mpsc, oneshot}; -use crate::imap::command::{anonymous, authenticated, selected}; +use crate::imap::command::{anonymous, authenticated, selected, examined}; use crate::imap::flow; use crate::login::ArcLoginProvider; @@ -127,6 +127,14 @@ impl Instance { }; selected::dispatch(ctx).await } + flow::State::Examined(ref user, ref mut mailbox) => { + let ctx = examined::ExaminedContext { + req: &msg.req, + user, + mailbox, + }; + examined::dispatch(ctx).await + } flow::State::Logout => { Response::bad("No commands are allowed in the LOGOUT state.") .map(|r| (r, flow::Transition::None))