aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/imap/command/anonymous.rs5
-rw-r--r--src/imap/command/selected.rs9
-rw-r--r--src/imap/mailbox_view.rs104
-rw-r--r--src/mail/mailbox.rs5
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<Vec<Body>> {
+ 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::<Body>::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<Flag> {
+ 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 {