Implement LIST
This commit is contained in:
parent
0e22a1c5d2
commit
ad15595f0f
4 changed files with 169 additions and 37 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 ---
|
||||||
|
|
||||||
|
|
|
@ -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::Existed(i, v) => (i, v),
|
||||||
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
|
self.tx_inbox_id.send(inbox_id).unwrap();
|
||||||
.send(Some((inbox_id, inbox_uidvalidity)))
|
}
|
||||||
.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>,
|
||||||
|
|
Loading…
Reference in a new issue