From a8d0e4a994daca39f9619cddf2847c1a7820c040 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 29 Jun 2022 17:58:31 +0200 Subject: [PATCH] Implement IDLE in selected state --- src/imap/command/anonymous.rs | 5 +- src/imap/command/selected.rs | 9 +++ src/imap/mailbox_view.rs | 104 +++++++++++++++++++++++++++++----- src/mail/mailbox.rs | 5 ++ 4 files changed, 106 insertions(+), 17 deletions(-) diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 84d0dca..b84b0da 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -21,10 +21,7 @@ pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response, flow:: CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, CommandBody::Login { username, password } => ctx.login(username, password).await, - _ => Ok(( - Response::no("This command is not available in the ANONYMOUS state.")?, - flow::Transition::None, - )), + _ => Ok((Response::no("Command unavailable")?, flow::Transition::None)), } } diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index cfc40c3..3a44a3f 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -21,6 +21,7 @@ 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 { sequence_set, attributes, @@ -47,4 +48,12 @@ impl<'a> SelectedContext<'a> { ) -> 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.update().await?; + Ok(( + Response::ok("Noop completed.")?.with_body(updates), + flow::Transition::None, + )) + } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 76ca80c..6066528 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1,10 +1,11 @@ +use std::num::NonZeroU32; use std::sync::Arc; use anyhow::{Error, Result}; use boitalettres::proto::res::body::Data as Body; use imap_codec::types::core::Atom; use imap_codec::types::flag::Flag; -use imap_codec::types::response::{Code, Data, Status}; +use imap_codec::types::response::{Code, Data, MessageAttribute, Status}; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::UidIndex; @@ -58,6 +59,79 @@ impl MailboxView { Ok((new_view, data)) } + /// 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> { + self.mailbox.sync().await?; + // TODO THIS IS JUST A TEST REMOVE LATER + self.mailbox.test().await?; + + let new_view = MailboxView { + mailbox: self.mailbox.clone(), + known_state: self.mailbox.current_uid_index().await, + }; + + let mut data = Vec::::new(); + + if new_view.known_state.uidvalidity != self.known_state.uidvalidity { + // TODO: do we want to push less/more info than this? + data.push(new_view.uidvalidity()?); + data.push(new_view.exists()?); + data.push(new_view.uidnext()?); + } else { + // Calculate diff between two mailbox states + // See example in IMAP RFC in section on NOOP command: + // we want to produce something like this: + // C: a047 NOOP + // S: * 22 EXPUNGE + // S: * 23 EXISTS + // S: * 14 FETCH (UID 1305 FLAGS (\Seen \Deleted)) + // S: a047 OK Noop completed + // In other words: + // - notify client of expunged mails + // - if new mails arrived, notify client of number of existing mails + // - if flags changed for existing mails, tell client + + // - notify client of expunged mails + let mut n_expunge = 0; + for (i, (uid, uuid)) in self.known_state.idx_by_uid.iter().enumerate() { + if !new_view.known_state.table.contains_key(uuid) { + data.push(Body::Data(Data::Expunge( + NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), + ))); + n_expunge += 1; + } + } + + // - if new mails arrived, notify client of number of existing mails + if new_view.known_state.table.len() != self.known_state.table.len() - n_expunge { + data.push(new_view.exists()?); + } + + // - if flags changed for existing mails, tell client + for (i, (uid, uuid)) in new_view.known_state.idx_by_uid.iter().enumerate() { + let old_mail = self.known_state.table.get(uuid); + let new_mail = new_view.known_state.table.get(uuid); + if old_mail.is_some() && old_mail != new_mail { + if let Some((uid, flags)) = new_mail { + data.push(Body::Data(Data::Fetch { + seq_or_uid: NonZeroU32::try_from((i + 1) as u32).unwrap(), + attributes: vec![ + MessageAttribute::Uid((*uid).try_into().unwrap()), + MessageAttribute::Flags( + flags.iter().filter_map(|f| string_to_flag(f)).collect(), + ), + ], + })); + } + } + } + } + + *self = new_view; + Ok(data) + } + // ---- /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` @@ -128,18 +202,7 @@ impl MailboxView { .known_state .idx_by_flag .flags() - .map(|f| match f.chars().next() { - Some('\\') => None, - Some('$') if f == "$unseen" => None, - Some(_) => match Atom::try_from(f.clone()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(Flag::Keyword(a)), - }, - None => None, - }) + .map(|f| string_to_flag(f)) .flatten() .collect(); for f in DEFAULT_FLAGS.iter() { @@ -158,3 +221,18 @@ impl MailboxView { Ok(ret) } } + +fn string_to_flag(f: &str) -> Option { + match f.chars().next() { + Some('\\') => None, + Some('$') if f == "$unseen" => None, + Some(_) => match Atom::try_from(f.clone()) { + Err(_) => { + tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); + None + } + Ok(a) => Some(Flag::Keyword(a)), + }, + None => None, + } +} diff --git a/src/mail/mailbox.rs b/src/mail/mailbox.rs index 0a4df57..6801ab7 100644 --- a/src/mail/mailbox.rs +++ b/src/mail/mailbox.rs @@ -36,6 +36,11 @@ impl Mailbox { Ok(Self { id, mbox }) } + /// Sync data with backing store + pub async fn sync(&self) -> Result<()> { + self.mbox.write().await.uid_index.sync().await + } + /// Get a clone of the current UID Index of this mailbox /// (cloning is cheap so don't hesitate to use this) pub async fn current_uid_index(&self) -> UidIndex {