diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index fda78c2..0582b06 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -4,9 +4,9 @@ use imap_codec::imap_types::core::AString; use imap_codec::imap_types::response::Code; use imap_codec::imap_types::secret::Secret; +use crate::imap::capability::ServerCapability; use crate::imap::command::anystate; use crate::imap::flow; -use crate::imap::capability::ServerCapability; use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -23,9 +23,9 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, f match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 7d7c0b2..718ba3f 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -2,11 +2,14 @@ use anyhow::Result; use imap_codec::imap_types::core::Tag; use imap_codec::imap_types::response::Data; -use crate::imap::flow; use crate::imap::capability::ServerCapability; +use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag<'static>, cap: &ServerCapability) -> Result<(Response<'static>, flow::Transition)> { +pub(crate) fn capability( + tag: Tag<'static>, + cap: &ServerCapability, +) -> Result<(Response<'static>, flow::Transition)> { let res = Response::build() .tag(tag) .message("Server capabilities") diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index a6a5203..2970b63 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -10,11 +10,11 @@ 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::capability::ServerCapability; use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; @@ -33,9 +33,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7cccf7b..bddd0f9 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,11 +7,11 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::user::User; pub struct ExaminedContext<'a> { @@ -25,10 +25,9 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities, - ), + 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) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 681b509..28ebbe8 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,11 +10,11 @@ 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::capability::ServerCapability; use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::user::User; @@ -31,10 +31,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities, - ), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) @@ -63,6 +62,11 @@ pub async fn dispatch<'a>( mailbox, uid, } => ctx.copy(sequence_set, mailbox, uid).await, + CommandBody::Move { + sequence_set, + mailbox, + uid, + } => ctx.r#move(sequence_set, mailbox, uid).await, // UNSELECT extension (rfc3691) CommandBody::Unselect => ctx.unselect().await, @@ -245,4 +249,58 @@ impl<'a> SelectedContext<'a> { flow::Transition::None, )) } + + async fn r#move( + self, + sequence_set: &SequenceSet, + mailbox: &MailboxCodec<'a>, + uid: &bool, + ) -> 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 { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .no()?, + flow::Transition::None, + )) + } + }; + + let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; + + // compute code + let copyuid_str = format!( + "{} {} {}", + uidval, + uid_map + .iter() + .map(|(sid, _)| format!("{}", sid)) + .collect::>() + .join(","), + uid_map + .iter() + .map(|(_, tuid)| format!("{}", tuid)) + .collect::>() + .join(",") + ); + + Ok(( + Response::build() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .set_body(data) + .ok()?, + flow::Transition::None, + )) + } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 7434512..206b905 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -600,6 +600,37 @@ impl MailboxView { Ok((to_state.uidvalidity, ret)) } + pub async fn r#move( + &mut self, + sequence_set: &SequenceSet, + to: Arc, + is_uid_copy: &bool, + ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec>)> { + let mails = self.get_mail_ids(sequence_set, *is_uid_copy)?; + + let mut new_uuids = vec![]; + for mi in mails.iter() { + let copy_action = to.copy_from(&self.mailbox, mi.uuid).await?; + new_uuids.push(copy_action); + self.mailbox.delete(mi.uuid).await? + } + + let mut ret = vec![]; + let to_state = to.current_uid_index().await; + for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { + let dest_uid = to_state + .table + .get(new_uuid) + .ok_or(anyhow!("moved mail not in destination mailbox"))? + .0; + ret.push((mi.uid, dest_uid)); + } + + let update = self.update().await?; + + Ok((to_state.uidvalidity, ret, update)) + } + /// Looks up state changes in the mailbox and produces a set of IMAP /// responses describing the new state. pub async fn fetch<'b>( diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 693c99a..0b5555a 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,9 +1,9 @@ +mod capability; mod command; mod flow; mod mailbox_view; mod response; mod session; -mod capability; use std::net::SocketAddr; @@ -102,14 +102,18 @@ async fn client(mut ctx: ClientContext) -> Result<()> { let (mut server, _) = ServerFlow::send_greeting( ctx.stream, ServerFlowOptions::default(), - Greeting::ok(Some(Code::Capability(ctx.server_capabilities.to_vec())), "Aerogramme").unwrap(), + Greeting::ok( + Some(Code::Capability(ctx.server_capabilities.to_vec())), + "Aerogramme", + ) + .unwrap(), ) .await?; use crate::imap::response::{Body, Response as MyResponse}; use crate::imap::session::Instance; use imap_codec::imap_types::command::Command; - use imap_codec::imap_types::response::{Response, Code, Status}; + use imap_codec::imap_types::response::{Code, Response, Status}; use tokio::sync::mpsc; let (cmd_tx, mut cmd_rx) = mpsc::channel::>(10); diff --git a/src/imap/session.rs b/src/imap/session.rs index c4f062f..55026b9 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,7 +1,7 @@ +use crate::imap::capability::ServerCapability; use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; use imap_codec::imap_types::command::Command; @@ -33,10 +33,11 @@ impl Instance { anonymous::dispatch(ctx).await } flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &cmd, + let ctx = authenticated::AuthenticatedContext { + req: &cmd, server_capabilities: &self.server_capabilities, - user }; + user, + }; authenticated::dispatch(ctx).await } flow::State::Selected(ref user, ref mut mailbox) => {