From 9b7d999fd52dc3410b97ca831238b084650be1b3 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 12 Jul 2022 15:59:13 +0200 Subject: [PATCH] Implement STATUS --- src/imap/command/authenticated.rs | 75 +++++++++++++++++++++++++----- src/imap/mailbox_view.rs | 76 +++++++++++++++++++------------ src/mail/unique_ident.rs | 8 +++- src/mail/user.rs | 11 ++++- 4 files changed, 127 insertions(+), 43 deletions(-) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index e7198ee..bc9fddf 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -2,11 +2,12 @@ use std::collections::BTreeMap; use std::sync::Arc; use anyhow::{anyhow, Result}; +use boitalettres::proto::res::body::Data as Body; use boitalettres::proto::{Request, Response}; use imap_codec::types::command::{CommandBody, StatusAttribute}; use imap_codec::types::flag::FlagNameAttribute; use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::types::response::{Code, Data}; +use imap_codec::types::response::{Code, Data, StatusAttributeValue}; use crate::imap::command::anonymous; use crate::imap::flow; @@ -165,26 +166,78 @@ impl<'a> AuthenticatedContext<'a> { async fn status( self, mailbox: &MailboxCodec, - _attributes: &[StatusAttribute], + attributes: &[StatusAttribute], ) -> Result<(Response, flow::Transition)> { - let _name = String::try_from(mailbox.clone())?; + 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, + )) + } + }; - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + let (view, _data) = MailboxView::new(mb).await?; + + let mut ret_attrs = vec![]; + for attr in attributes.iter() { + ret_attrs.push(match attr { + StatusAttribute::Messages => StatusAttributeValue::Messages(view.exists()?), + StatusAttribute::Unseen => { + StatusAttributeValue::Unseen(view.unseen().map(|x| x.get()).unwrap_or(0)) + } + StatusAttribute::Recent => StatusAttributeValue::Recent(view.recent()?), + StatusAttribute::UidNext => StatusAttributeValue::UidNext(view.uidnext()), + StatusAttribute::UidValidity => { + StatusAttributeValue::UidValidity(view.uidvalidity()) + } + }); + } + + let data = vec![Body::Data(Data::Status { + mailbox: mailbox.clone(), + attributes: ret_attrs, + })]; + + Ok(( + Response::ok("STATUS completed")?.with_body(data), + flow::Transition::None, + )) } async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let _name = String::try_from(mailbox.clone())?; + let name = String::try_from(mailbox.clone())?; - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + if self.user.has_mailbox(&name).await? { + Ok((Response::ok("SUBSCRIBE complete")?, flow::Transition::None)) + } else { + Ok(( + Response::bad(&format!("Mailbox {} does not exist", name))?, + flow::Transition::None, + )) + } } async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let _name = String::try_from(mailbox.clone())?; + let name = String::try_from(mailbox.clone())?; - Ok(( - Response::bad("Aerogramme does not support unsubscribing from a mailbox")?, - flow::Transition::None, - )) + if self.user.has_mailbox(&name).await? { + Ok(( + Response::bad(&format!( + "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", + name + ))?, + flow::Transition::None, + )) + } else { + Ok(( + Response::bad(&format!("Mailbox {} does not exist", name))?, + flow::Transition::None, + )) + } } /* diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 0adba18..db6f490 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -19,7 +19,7 @@ use imap_codec::types::sequence::{self, SequenceSet}; use mail_parser::*; use crate::mail::mailbox::Mailbox; -use crate::mail::uidindex::UidIndex; +use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex}; const DEFAULT_FLAGS: [Flag; 5] = [ Flag::Seen, @@ -55,12 +55,12 @@ impl MailboxView { }; let mut data = Vec::::new(); - data.push(new_view.exists()?); - data.push(new_view.recent()?); - data.extend(new_view.flags()?.into_iter()); - data.push(new_view.uidvalidity()?); - data.push(new_view.uidnext()?); - if let Some(unseen) = new_view.unseen()? { + data.push(new_view.exists_status()?); + data.push(new_view.recent_status()?); + data.extend(new_view.flags_status()?.into_iter()); + data.push(new_view.uidvalidity_status()?); + data.push(new_view.uidnext_status()?); + if let Some(unseen) = new_view.unseen_status()? { data.push(unseen); } @@ -87,9 +87,9 @@ impl MailboxView { 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()?); + data.push(new_view.uidvalidity_status()?); + data.push(new_view.exists_status()?); + data.push(new_view.uidnext_status()?); } else { // Calculate diff between two mailbox states // See example in IMAP RFC in section on NOOP command: @@ -117,7 +117,7 @@ impl MailboxView { // - 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()?); + data.push(new_view.exists_status()?); } // - if flags changed for existing mails, tell client @@ -315,37 +315,39 @@ impl MailboxView { // ---- /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` - fn uidvalidity(&self) -> Result { + fn uidvalidity_status(&self) -> Result { let uid_validity = Status::ok( None, - Some(Code::UidValidity(self.known_state.uidvalidity)), + Some(Code::UidValidity(self.uidvalidity())), "UIDs valid", ) .map_err(Error::msg)?; Ok(Body::Status(uid_validity)) } + pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { + self.known_state.uidvalidity + } + /// Produce an OK [UIDNEXT _] message corresponding to `known_state` - fn uidnext(&self) -> Result { + fn uidnext_status(&self) -> Result { let next_uid = Status::ok( None, - Some(Code::UidNext(self.known_state.uidnext)), + Some(Code::UidNext(self.uidnext())), "Predict next UID", ) .map_err(Error::msg)?; Ok(Body::Status(next_uid)) } + pub(crate) fn uidnext(&self) -> ImapUid { + self.known_state.uidnext + } + /// Produces an UNSEEN message (if relevant) corresponding to the /// first unseen message id in `known_state` - fn unseen(&self) -> Result> { - let unseen = self - .known_state - .idx_by_flag - .get(&"$unseen".to_string()) - .and_then(|os| os.get_min()) - .cloned(); - if let Some(unseen) = unseen { + fn unseen_status(&self) -> Result> { + if let Some(unseen) = self.unseen() { let status_unseen = Status::ok(None, Some(Code::Unseen(unseen.clone())), "First unseen UID") .map_err(Error::msg)?; @@ -355,29 +357,43 @@ impl MailboxView { } } + pub(crate) fn unseen(&self) -> Option { + self.known_state + .idx_by_flag + .get(&"$unseen".to_string()) + .and_then(|os| os.get_min()) + .cloned() + } + /// Produce an EXISTS message corresponding to the number of mails /// in `known_state` - fn exists(&self) -> Result { - let exists = u32::try_from(self.known_state.idx_by_uid.len())?; - Ok(Body::Data(Data::Exists(exists))) + fn exists_status(&self) -> Result { + Ok(Body::Data(Data::Exists(self.exists()?))) + } + + pub(crate) fn exists(&self) -> Result { + Ok(u32::try_from(self.known_state.idx_by_uid.len())?) } /// Produce a RECENT message corresponding to the number of /// recent mails in `known_state` - fn recent(&self) -> Result { + fn recent_status(&self) -> Result { + Ok(Body::Data(Data::Recent(self.recent()?))) + } + + pub(crate) fn recent(&self) -> Result { let recent = self .known_state .idx_by_flag .get(&"\\Recent".to_string()) .map(|os| os.len()) .unwrap_or(0); - let recent = u32::try_from(recent)?; - Ok(Body::Data(Data::Recent(recent))) + Ok(u32::try_from(recent)?) } /// Produce a FLAGS and a PERMANENTFLAGS message that indicates /// the flags that are in `known_state` + default flags - fn flags(&self) -> Result> { + fn flags_status(&self) -> Result> { let mut flags: Vec = self .known_state .idx_by_flag diff --git a/src/mail/unique_ident.rs b/src/mail/unique_ident.rs index 19de6fc..267f66e 100644 --- a/src/mail/unique_ident.rs +++ b/src/mail/unique_ident.rs @@ -16,7 +16,7 @@ use crate::time::now_msec; /// required by Aerogramme. /// Their main property is to be unique without having to rely /// on synchronization between IMAP processes. -#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct UniqueIdent(pub [u8; 24]); struct IdentGenerator { @@ -78,6 +78,12 @@ impl std::fmt::Display for UniqueIdent { } } +impl std::fmt::Debug for UniqueIdent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + impl FromStr for UniqueIdent { type Err = &'static str; diff --git a/src/mail/user.rs b/src/mail/user.rs index e80931f..fd858d3 100644 --- a/src/mail/user.rs +++ b/src/mail/user.rs @@ -71,7 +71,10 @@ impl User { /// Opens an existing mailbox given its IMAP name. pub async fn open_mailbox(&self, name: &str) -> Result>> { let (mut list, ct) = self.load_mailbox_list().await?; - eprintln!("List of mailboxes: {:?}", &list.0); + eprintln!("List of mailboxes:"); + for ent in list.0.iter() { + eprintln!(" - {:?}", ent); + } if let Some((uidvalidity, Some(mbid))) = list.get_mailbox(name) { let mb_opt = self.open_mailbox_by_id(mbid, uidvalidity).await?; @@ -88,6 +91,12 @@ impl User { } } + /// Check whether mailbox exists + pub async fn has_mailbox(&self, name: &str) -> Result { + let (list, _ct) = self.load_mailbox_list().await?; + Ok(list.has_mailbox(name)) + } + /// Creates a new mailbox in the user's IMAP namespace. pub async fn create_mailbox(&self, name: &str) -> Result<()> { if name.ends_with(MAILBOX_HIERARCHY_DELIMITER) {