diff --git a/Cargo.lock b/Cargo.lock index b2d5365..11b1d86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "clap", "duplexify", "futures", + "globset", "hex", "im", "imap-codec", @@ -443,6 +444,15 @@ dependencies = [ "tracing-futures", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -963,6 +973,19 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "gloo-timers" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 5398f34..dd6cf68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ clap = { version = "3.1.18", features = ["derive", "env"] } duplexify = "1.1.0" hex = "0.4" futures = "0.3" +globset = "0.4" im = "15" itertools = "0.10" lazy_static = "1.4" diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 0b34223..f46dfb4 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -1,16 +1,18 @@ +use std::collections::BTreeMap; use std::sync::Arc; -use anyhow::Result; +use anyhow::{anyhow, Result}; 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; +use imap_codec::types::response::{Code, Data}; use crate::imap::command::anonymous; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; -use crate::mail::user::User; +use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER}; pub struct AuthenticatedContext<'a> { pub req: &'a Request, @@ -28,15 +30,17 @@ pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, fl CommandBody::Lsub { reference, mailbox_wildcard, - } => ctx.lsub(reference, mailbox_wildcard).await, + } => ctx.list(reference, mailbox_wildcard, true).await, CommandBody::List { reference, mailbox_wildcard, - } => ctx.list(reference, mailbox_wildcard).await, + } => ctx.list(reference, mailbox_wildcard, false).await, CommandBody::Status { mailbox, attributes, } => ctx.status(mailbox, attributes).await, + CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, + CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await, CommandBody::Examine { mailbox } => ctx.examine(mailbox).await, _ => { @@ -53,11 +57,28 @@ pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, fl impl<'a> AuthenticatedContext<'a> { async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + let name = String::try_from(mailbox.clone())?; + + if name == INBOX { + return Ok(( + Response::bad("Cannot create INBOX")?, + flow::Transition::None, + )); + } + + match self.user.create_mailbox(&name).await { + Ok(()) => Ok((Response::ok("CREATE complete")?, flow::Transition::None)), + Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + } } async fn delete(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + let name = String::try_from(mailbox.clone())?; + + match self.user.delete_mailbox(&name).await { + Ok(()) => Ok((Response::ok("DELETE complete")?, flow::Transition::None)), + Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + } } async fn rename( @@ -65,23 +86,80 @@ impl<'a> AuthenticatedContext<'a> { mailbox: &MailboxCodec, new_mailbox: &MailboxCodec, ) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) - } + let name = String::try_from(mailbox.clone())?; + let new_name = String::try_from(new_mailbox.clone())?; - async fn lsub( - self, - _reference: &MailboxCodec, - _mailbox_wildcard: &ListMailbox, - ) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + match self.user.rename_mailbox(&name, &new_name).await { + Ok(()) => Ok((Response::ok("RENAME complete")?, flow::Transition::None)), + Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), + } } async fn list( self, - _reference: &MailboxCodec, - _mailbox_wildcard: &ListMailbox, + reference: &MailboxCodec, + mailbox_wildcard: &ListMailbox, + is_lsub: bool, ) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + let reference = String::try_from(reference.clone())?; + if !reference.is_empty() { + return Ok(( + Response::bad("References not supported")?, + flow::Transition::None, + )); + } + + let mailboxes = self.user.list_mailboxes().await?; + let mut vmailboxes = BTreeMap::new(); + for mb in mailboxes.iter() { + for (i, _) in mb.match_indices(MAILBOX_HIERARCHY_DELIMITER) { + if i > 0 { + let smb = &mb[..i]; + if !vmailboxes.contains_key(&smb) { + vmailboxes.insert(smb, false); + } + } + } + vmailboxes.insert(mb, true); + } + + let wildcard = String::try_from(mailbox_wildcard.clone())?; + let wildcard_pat = globset::Glob::new(&wildcard)?.compile_matcher(); + + let mut ret = vec![]; + for (mb, is_real) in vmailboxes.iter() { + if wildcard_pat.is_match(mb) { + let mailbox = mb + .to_string() + .try_into() + .map_err(|_| anyhow!("invalid mailbox name"))?; + let mut items = vec![]; + if !*is_real { + items.push(FlagNameAttribute::Noselect); + } + if is_lsub { + items.push(FlagNameAttribute::Extension( + "\\Subscribed".try_into().unwrap(), + )); + ret.push(Data::Lsub { + items, + delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + mailbox, + }); + } else { + ret.push(Data::List { + items, + delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + mailbox, + }); + } + } + } + + Ok(( + Response::ok("LIST completed")?.with_body(ret), + flow::Transition::None, + )) } async fn status( @@ -89,9 +167,26 @@ impl<'a> AuthenticatedContext<'a> { mailbox: &MailboxCodec, attributes: &[StatusAttribute], ) -> Result<(Response, flow::Transition)> { + let name = String::try_from(mailbox.clone())?; + Ok((Response::bad("Not implemented")?, flow::Transition::None)) } + async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { + let name = String::try_from(mailbox.clone())?; + + Ok((Response::bad("Not implemented")?, flow::Transition::None)) + } + + async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { + let name = String::try_from(mailbox.clone())?; + + Ok(( + Response::bad("Aerogramme does not support unsubscribing from a mailbox")?, + flow::Transition::None, + )) + } + /* * TRACE BEGIN --- diff --git a/src/mail/user.rs b/src/mail/user.rs index c2d1d85..aeec630 100644 --- a/src/mail/user.rs +++ b/src/mail/user.rs @@ -15,7 +15,7 @@ use crate::mail::uidindex::ImapUidvalidity; use crate::mail::unique_ident::{gen_ident, UniqueIdent}; use crate::time::now_msec; -const MAILBOX_HIERARCHY_DELIMITER: &str = "/"; +pub const MAILBOX_HIERARCHY_DELIMITER: char = '.'; /// INBOX is the only mailbox that must always exist. /// It is created automatically when the account is created. @@ -25,7 +25,7 @@ const MAILBOX_HIERARCHY_DELIMITER: &str = "/"; /// In our implementation, we indeed move the underlying mailbox /// to the new name (i.e. the new name has the same id as the previous /// INBOX), and we create a new empty mailbox for INBOX. -const INBOX: &str = "INBOX"; +pub const INBOX: &str = "INBOX"; const MAILBOX_LIST_PK: &str = "mailboxes"; const MAILBOX_LIST_SK: &str = "list"; @@ -94,20 +94,24 @@ impl User { /// Creates a new mailbox in the user's IMAP namespace. pub async fn create_mailbox(&self, name: &str) -> Result<()> { let (mut list, ct) = self.load_mailbox_list().await?; - match self.create_mailbox_internal(&mut list, ct, name).await? { + match self.mblist_create_mailbox(&mut list, ct, name).await? { CreatedMailbox::Created(_, _) => Ok(()), CreatedMailbox::Existed(_, _) => Err(anyhow!("Mailbox {} already exists", name)), } } /// Deletes a mailbox in the user's IMAP namespace. - pub fn delete_mailbox(&self, _name: &str) -> Result<()> { - unimplemented!() + pub async fn delete_mailbox(&self, _name: &str) -> Result<()> { + bail!("Deleting mailboxes not implemented yet") } /// Renames a mailbox in the user's IMAP namespace. - pub fn rename_mailbox(&self, _old_name: &str, _new_name: &str) -> Result<()> { - unimplemented!() + pub async fn rename_mailbox(&self, old_name: &str, new_name: &str) -> Result<()> { + if old_name == INBOX { + bail!("Renaming INBOX not implemented yet") + } else { + bail!("Renaming not implemented yet") + } } // ---- Internal user & mailbox management ---- @@ -180,22 +184,31 @@ impl User { } }; + self.ensure_inbox_exists(&mut list, &ct).await?; + + Ok((list, ct)) + } + + async fn ensure_inbox_exists( + &self, + list: &mut MailboxList, + ct: &Option, + ) -> Result<()> { // If INBOX doesn't exist, create a new mailbox with that name // and save new mailbox list. // Also, ensure that the mpsc::watch that keeps track of the // inbox id is up-to-date. - let (inbox_id, inbox_uidvalidity) = match self - .create_mailbox_internal(&mut list, ct.clone(), INBOX) - .await? - { - CreatedMailbox::Created(i, v) => (i, v), - CreatedMailbox::Existed(i, v) => (i, v), - }; - self.tx_inbox_id - .send(Some((inbox_id, inbox_uidvalidity))) - .unwrap(); + let (inbox_id, inbox_uidvalidity) = + match self.mblist_create_mailbox(list, ct.clone(), INBOX).await? { + CreatedMailbox::Created(i, v) => (i, v), + CreatedMailbox::Existed(i, v) => (i, v), + }; + let inbox_id = Some((inbox_id, inbox_uidvalidity)); + if *self.tx_inbox_id.borrow() != inbox_id { + self.tx_inbox_id.send(inbox_id).unwrap(); + } - Ok((list, ct)) + Ok(()) } async fn save_mailbox_list( @@ -210,7 +223,7 @@ impl User { Ok(()) } - async fn create_mailbox_internal( + async fn mblist_create_mailbox( &self, list: &mut MailboxList, ct: Option,