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",
|
||||
"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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ---
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Reference in a new issue