Implement LIST

This commit is contained in:
Alex 2022-07-12 13:19:27 +02:00
parent 0e22a1c5d2
commit ad15595f0f
Signed by: lx
GPG key ID: 0E496D15096376BE
4 changed files with 169 additions and 37 deletions

23
Cargo.lock generated
View file

@ -30,6 +30,7 @@ dependencies = [
"clap", "clap",
"duplexify", "duplexify",
"futures", "futures",
"globset",
"hex", "hex",
"im", "im",
"imap-codec", "imap-codec",
@ -443,6 +444,15 @@ dependencies = [
"tracing-futures", "tracing-futures",
] ]
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.10.0" version = "3.10.0"
@ -963,6 +973,19 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1", "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]] [[package]]
name = "gloo-timers" name = "gloo-timers"
version = "0.2.4" version = "0.2.4"

View file

@ -15,6 +15,7 @@ clap = { version = "3.1.18", features = ["derive", "env"] }
duplexify = "1.1.0" duplexify = "1.1.0"
hex = "0.4" hex = "0.4"
futures = "0.3" futures = "0.3"
globset = "0.4"
im = "15" im = "15"
itertools = "0.10" itertools = "0.10"
lazy_static = "1.4" lazy_static = "1.4"

View file

@ -1,16 +1,18 @@
use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::{anyhow, Result};
use boitalettres::proto::{Request, Response}; use boitalettres::proto::{Request, Response};
use imap_codec::types::command::{CommandBody, StatusAttribute}; 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::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::command::anonymous;
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; 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 struct AuthenticatedContext<'a> {
pub req: &'a Request, pub req: &'a Request,
@ -28,15 +30,17 @@ pub async fn dispatch<'a>(ctx: AuthenticatedContext<'a>) -> Result<(Response, fl
CommandBody::Lsub { CommandBody::Lsub {
reference, reference,
mailbox_wildcard, mailbox_wildcard,
} => ctx.lsub(reference, mailbox_wildcard).await, } => ctx.list(reference, mailbox_wildcard, true).await,
CommandBody::List { CommandBody::List {
reference, reference,
mailbox_wildcard, mailbox_wildcard,
} => ctx.list(reference, mailbox_wildcard).await, } => ctx.list(reference, mailbox_wildcard, false).await,
CommandBody::Status { CommandBody::Status {
mailbox, mailbox,
attributes, attributes,
} => ctx.status(mailbox, attributes).await, } => 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::Select { mailbox } => ctx.select(mailbox).await,
CommandBody::Examine { mailbox } => ctx.examine(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> { impl<'a> AuthenticatedContext<'a> {
async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { 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)> { 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( async fn rename(
@ -65,23 +86,80 @@ impl<'a> AuthenticatedContext<'a> {
mailbox: &MailboxCodec, mailbox: &MailboxCodec,
new_mailbox: &MailboxCodec, new_mailbox: &MailboxCodec,
) -> Result<(Response, flow::Transition)> { ) -> 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( match self.user.rename_mailbox(&name, &new_name).await {
self, Ok(()) => Ok((Response::ok("RENAME complete")?, flow::Transition::None)),
_reference: &MailboxCodec, Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)),
_mailbox_wildcard: &ListMailbox, }
) -> Result<(Response, flow::Transition)> {
Ok((Response::bad("Not implemented")?, flow::Transition::None))
} }
async fn list( async fn list(
self, self,
_reference: &MailboxCodec, reference: &MailboxCodec,
_mailbox_wildcard: &ListMailbox, mailbox_wildcard: &ListMailbox,
is_lsub: bool,
) -> Result<(Response, flow::Transition)> { ) -> 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( async fn status(
@ -89,9 +167,26 @@ impl<'a> AuthenticatedContext<'a> {
mailbox: &MailboxCodec, mailbox: &MailboxCodec,
attributes: &[StatusAttribute], attributes: &[StatusAttribute],
) -> Result<(Response, flow::Transition)> { ) -> Result<(Response, flow::Transition)> {
let name = String::try_from(mailbox.clone())?;
Ok((Response::bad("Not implemented")?, flow::Transition::None)) 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 --- * TRACE BEGIN ---

View file

@ -15,7 +15,7 @@ use crate::mail::uidindex::ImapUidvalidity;
use crate::mail::unique_ident::{gen_ident, UniqueIdent}; use crate::mail::unique_ident::{gen_ident, UniqueIdent};
use crate::time::now_msec; 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. /// INBOX is the only mailbox that must always exist.
/// It is created automatically when the account is created. /// 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 /// 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 /// 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. /// 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_PK: &str = "mailboxes";
const MAILBOX_LIST_SK: &str = "list"; const MAILBOX_LIST_SK: &str = "list";
@ -94,20 +94,24 @@ impl User {
/// Creates a new mailbox in the user's IMAP namespace. /// Creates a new mailbox in the user's IMAP namespace.
pub async fn create_mailbox(&self, name: &str) -> Result<()> { pub async fn create_mailbox(&self, name: &str) -> Result<()> {
let (mut list, ct) = self.load_mailbox_list().await?; 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::Created(_, _) => Ok(()),
CreatedMailbox::Existed(_, _) => Err(anyhow!("Mailbox {} already exists", name)), CreatedMailbox::Existed(_, _) => Err(anyhow!("Mailbox {} already exists", name)),
} }
} }
/// Deletes a mailbox in the user's IMAP namespace. /// Deletes a mailbox in the user's IMAP namespace.
pub fn delete_mailbox(&self, _name: &str) -> Result<()> { pub async fn delete_mailbox(&self, _name: &str) -> Result<()> {
unimplemented!() bail!("Deleting mailboxes not implemented yet")
} }
/// Renames a mailbox in the user's IMAP namespace. /// Renames a mailbox in the user's IMAP namespace.
pub fn rename_mailbox(&self, _old_name: &str, _new_name: &str) -> Result<()> { pub async fn rename_mailbox(&self, old_name: &str, new_name: &str) -> Result<()> {
unimplemented!() if old_name == INBOX {
bail!("Renaming INBOX not implemented yet")
} else {
bail!("Renaming not implemented yet")
}
} }
// ---- Internal user & mailbox management ---- // ---- 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 // If INBOX doesn't exist, create a new mailbox with that name
// and save new mailbox list. // and save new mailbox list.
// Also, ensure that the mpsc::watch that keeps track of the // Also, ensure that the mpsc::watch that keeps track of the
// inbox id is up-to-date. // inbox id is up-to-date.
let (inbox_id, inbox_uidvalidity) = match self let (inbox_id, inbox_uidvalidity) =
.create_mailbox_internal(&mut list, ct.clone(), INBOX) match self.mblist_create_mailbox(list, ct.clone(), INBOX).await? {
.await?
{
CreatedMailbox::Created(i, v) => (i, v), CreatedMailbox::Created(i, v) => (i, v),
CreatedMailbox::Existed(i, v) => (i, v), CreatedMailbox::Existed(i, v) => (i, v),
}; };
self.tx_inbox_id let inbox_id = Some((inbox_id, inbox_uidvalidity));
.send(Some((inbox_id, inbox_uidvalidity))) if *self.tx_inbox_id.borrow() != inbox_id {
.unwrap(); self.tx_inbox_id.send(inbox_id).unwrap();
}
Ok((list, ct)) Ok(())
} }
async fn save_mailbox_list( async fn save_mailbox_list(
@ -210,7 +223,7 @@ impl User {
Ok(()) Ok(())
} }
async fn create_mailbox_internal( async fn mblist_create_mailbox(
&self, &self,
list: &mut MailboxList, list: &mut MailboxList,
ct: Option<CausalityToken>, ct: Option<CausalityToken>,