aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2022-07-12 13:19:27 +0200
committerAlex Auvolat <alex@adnab.me>2022-07-12 13:19:27 +0200
commitad15595f0fc26f4dca58332a85b5e88ead67e359 (patch)
treee5c4460b42a17c5b0093650ee284a257d2282624
parent0e22a1c5d2eb9b031497cb5314890ecc0cee3901 (diff)
downloadaerogramme-ad15595f0fc26f4dca58332a85b5e88ead67e359.tar.gz
aerogramme-ad15595f0fc26f4dca58332a85b5e88ead67e359.zip
Implement LIST
-rw-r--r--Cargo.lock23
-rw-r--r--Cargo.toml1
-rw-r--r--src/imap/command/authenticated.rs131
-rw-r--r--src/mail/user.rs51
4 files changed, 169 insertions, 37 deletions
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",
@@ -444,6 +445,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -964,6 +974,19 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<CausalityToken>,
+ ) -> 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<CausalityToken>,