From 9ae5701c7c6d17c72f27f1413ee2fd3d939428a3 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 20 Jan 2024 18:34:37 +0100 Subject: [PATCH 1/2] Implement `LIST X Y RETURN (STATUS (UIDNEXT ...))` --- Cargo.lock | 49 +++++++------ Cargo.toml | 1 + mailrage.toml | 13 ---- src/imap/command/authenticated.rs | 118 ++++++++++++++++++++++-------- 4 files changed, 112 insertions(+), 69 deletions(-) delete mode 100644 mailrage.toml diff --git a/Cargo.lock b/Cargo.lock index afa6980..ea9946b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,7 @@ dependencies = [ "smtp-message", "smtp-server", "sodiumoxide", + "thiserror", "tokio", "tokio-util", "toml", @@ -365,7 +366,7 @@ checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -990,7 +991,7 @@ checksum = "f10dd247355bf631d98d2753d87ae62c84c8dcb996ad9b24a4168e0aec29bd6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1258,7 +1259,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1522,7 +1523,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1723,7 +1724,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite 0.2.13", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1807,7 +1808,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#0f27fe2f10d16c96e0be18914fdbeda9df545beb" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#0adcc244282c64cc7874ffa9cd22e4a451ee19f8" dependencies = [ "abnf-core", "base64 0.21.5", @@ -1834,7 +1835,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#0f27fe2f10d16c96e0be18914fdbeda9df545beb" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#0adcc244282c64cc7874ffa9cd22e4a451ee19f8" dependencies = [ "base64 0.21.5", "bounded-static", @@ -2283,7 +2284,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -2399,18 +2400,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2793,7 +2794,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3022,9 +3023,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3066,22 +3067,22 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3164,7 +3165,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3248,7 +3249,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -3436,7 +3437,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3470,7 +3471,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 55f0284..b5158b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] } imap-flow = { git = "https://github.com/superboum/imap-flow.git", branch = "custom/aerogramme" } +thiserror = "1.0.56" [dev-dependencies] diff --git a/mailrage.toml b/mailrage.toml deleted file mode 100644 index 4cba391..0000000 --- a/mailrage.toml +++ /dev/null @@ -1,13 +0,0 @@ -s3_endpoint = "http://[::1]:3900" -k2v_endpoint = "http://[::1]:3904" -aws_region = "garage" - -[imap] -bind_addr = "[::1]:4567" - -[login_static.users.quentin] -password = "$argon2id$v=19$m=4096,t=3,p=1$jR52Nq76f8yO0UXdhK+FiQ$KeIzDI4PJ/2bX+expyyaRkMZus0/1FsgTXtnvPUjwyw" -aws_access_key_id = "GK68198c3b4148f61dcd625b7e" -aws_secret_access_key = "1d4bd3853a4f7810b97cbb2f8eb52c7603eb93c202fe98ca40f4e3f6b7e70fa0" -user_secret = "poupou" -bucket = "quentin-mailrage" diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 60872ae..26b1946 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -1,8 +1,11 @@ use std::collections::BTreeMap; use std::sync::Arc; +use thiserror::Error; use anyhow::{anyhow, bail, Result}; -use imap_codec::imap_types::command::{Command, CommandBody, SelectExamineModifier}; +use imap_codec::imap_types::command::{ + Command, CommandBody, ListReturnItem, SelectExamineModifier, +}; use imap_codec::imap_types::core::{Atom, Literal, NonEmptyVec, QuotedChar}; use imap_codec::imap_types::datetime::DateTime; use imap_codec::imap_types::extensions::enable::CapabilityEnable; @@ -30,7 +33,7 @@ pub struct AuthenticatedContext<'a> { } pub async fn dispatch<'a>( - ctx: AuthenticatedContext<'a>, + mut ctx: AuthenticatedContext<'a>, ) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any state @@ -47,11 +50,12 @@ pub async fn dispatch<'a>( CommandBody::Lsub { reference, mailbox_wildcard, - } => ctx.list(reference, mailbox_wildcard, true).await, + } => ctx.list(reference, mailbox_wildcard, &[], true).await, CommandBody::List { reference, mailbox_wildcard, - } => ctx.list(reference, mailbox_wildcard, false).await, + r#return, + } => ctx.list(reference, mailbox_wildcard, r#return, false).await, CommandBody::Status { mailbox, item_names, @@ -163,9 +167,10 @@ impl<'a> AuthenticatedContext<'a> { } async fn list( - self, + &mut self, reference: &MailboxCodec<'a>, mailbox_wildcard: &ListMailbox<'a>, + must_return: &[ListReturnItem], is_lsub: bool, ) -> Result<(Response<'static>, flow::Transition)> { let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); @@ -181,6 +186,11 @@ impl<'a> AuthenticatedContext<'a> { )); } + let status_item_names = must_return.iter().find_map(|m| match m { + ListReturnItem::Status(v) => Some(v), + _ => None, + }); + // @FIXME would probably need a rewrite to better use the imap_codec library let wildcard = match mailbox_wildcard { ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, @@ -231,11 +241,13 @@ impl<'a> AuthenticatedContext<'a> { let mut ret = vec![]; for (mb, is_real) in vmailboxes.iter() { if matches_wildcard(&wildcard, mb) { - let mailbox = mb + let mailbox: MailboxCodec = mb .to_string() .try_into() .map_err(|_| anyhow!("invalid mailbox name"))?; let mut items = vec![FlagNameAttribute::from(Atom::unvalidated("Subscribed"))]; + + // Decoration if !*is_real { items.push(FlagNameAttribute::Noselect); } else { @@ -247,19 +259,39 @@ impl<'a> AuthenticatedContext<'a> { _ => (), }; } + + // Result type if is_lsub { ret.push(Data::Lsub { items, delimiter: Some(mbx_hier_delim), - mailbox, + mailbox: mailbox.clone(), }); } else { ret.push(Data::List { items, delimiter: Some(mbx_hier_delim), - mailbox, + mailbox: mailbox.clone(), }); } + + // Also collect status + if let Some(sin) = status_item_names { + let ret_attrs = match self.status_items(mb, sin).await { + Ok(a) => a, + Err(e) => { + tracing::error!(err=?e, mailbox=%mb, "Unable to fetch status for mailbox"); + continue; + } + }; + + let data = Data::Status { + mailbox, + items: ret_attrs.into(), + }; + + ret.push(data); + } } } @@ -279,23 +311,52 @@ impl<'a> AuthenticatedContext<'a> { } async fn status( - self, + &mut self, mailbox: &MailboxCodec<'static>, attributes: &[StatusDataItemName], ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; + + let ret_attrs = match self.status_items(name, attributes).await { + Ok(v) => v, + Err(e) => match e.downcast_ref::() { + Some(CommandError::MailboxNotFound) => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Mailbox does not exist") + .no()?, + flow::Transition::None, + )) + } + _ => return Err(e.into()), + }, + }; + + let data = Data::Status { + mailbox: mailbox.clone(), + items: ret_attrs.into(), + }; + + Ok(( + Response::build() + .to_req(self.req) + .message("STATUS completed") + .data(data) + .ok()?, + flow::Transition::None, + )) + } + + async fn status_items( + &mut self, + name: &str, + attributes: &[StatusDataItemName], + ) -> Result> { let mb_opt = self.user.open_mailbox(name).await?; let mb = match mb_opt { Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } + None => return Err(CommandError::MailboxNotFound.into()), }; let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; @@ -322,20 +383,7 @@ impl<'a> AuthenticatedContext<'a> { }, }); } - - let data = Data::Status { - mailbox: mailbox.clone(), - items: ret_attrs.into(), - }; - - Ok(( - Response::build() - .to_req(self.req) - .message("STATUS completed") - .data(data) - .ok()?, - flow::Transition::None, - )) + Ok(ret_attrs) } async fn subscribe( @@ -604,6 +652,12 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool { matches[name.len()][wildcard.len()] } +#[derive(Error, Debug)] +pub enum CommandError { + #[error("Mailbox not found")] + MailboxNotFound, +} + #[cfg(test)] mod tests { use super::*; -- 2.43.4 From 9c3f44748051ce15607af3470e5d4d29abaecc37 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 20 Jan 2024 19:23:44 +0100 Subject: [PATCH 2/2] Test LIST-STATUS --- src/imap/capability.rs | 5 ++++ tests/behavior.rs | 56 ++++++++++++++++++++++++++++++++++++--- tests/common/fragments.rs | 30 +++++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 525b3ef..256d820 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -18,6 +18,10 @@ fn capability_uidplus() -> Capability<'static> { Capability::try_from("UIDPLUS").unwrap() } +fn capability_liststatus() -> Capability<'static> { + Capability::try_from("LIST-STATUS").unwrap() +} + /* fn capability_qresync() -> Capability<'static> { Capability::try_from("QRESYNC").unwrap() @@ -38,6 +42,7 @@ impl Default for ServerCapability { capability_unselect(), capability_condstore(), capability_uidplus(), + capability_liststatus(), //capability_qresync(), ])) } diff --git a/tests/behavior.rs b/tests/behavior.rs index 0447d80..7fdd553 100644 --- a/tests/behavior.rs +++ b/tests/behavior.rs @@ -6,13 +6,14 @@ use crate::common::fragments::*; fn main() { rfc3501_imap4rev1_base(); - rfc3691_imapext_unselect(); - rfc5161_imapext_enable(); rfc6851_imapext_move(); - rfc7888_imapext_literal(); rfc4551_imapext_condstore(); rfc2177_imapext_idle(); - rfc4315_imapext_uidplus(); + rfc5161_imapext_enable(); // 1 + rfc3691_imapext_unselect(); // 2 + rfc7888_imapext_literal(); // 3 + rfc4315_imapext_uidplus(); // 4 + rfc5819_imapext_liststatus(); // 5 println!("โœ… SUCCESS ๐ŸŒŸ๐Ÿš€๐Ÿฅณ๐Ÿ™๐Ÿฅน"); } @@ -307,3 +308,50 @@ fn rfc4315_imapext_uidplus() { }) .expect("test fully run"); } + +/// +/// Example +/// +/// ```text +/// 30 list "" "*" RETURN (STATUS (MESSAGES UNSEEN)) +/// * LIST (\Subscribed) "." INBOX +/// * STATUS INBOX (MESSAGES 2 UNSEEN 1) +/// 30 OK LIST completed +/// ``` +fn rfc5819_imapext_liststatus() { + println!("๐Ÿงช rfc5819_imapext_liststatus"); + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + // Test setup, check capability, add 2 emails, read 1 + connect(imap_socket).context("server says hello")?; + capability(imap_socket, Extension::ListStatus).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?; + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; + noop_exists(imap_socket, 2).context("noop loop must detect a new email")?; + fetch( + imap_socket, + Selection::FirstId, + FetchKind::Rfc822, + FetchMod::None, + ) + .context("read one message")?; + close(imap_socket).context("close inbox")?; + + // Test return status MESSAGES UNSEEN + let ret = list( + imap_socket, + MbxSelect::All, + ListReturn::StatusMessagesUnseen, + )?; + assert!(ret.contains("* STATUS INBOX (MESSAGES 2 UNSEEN 1)")); + + // Test that without RETURN, no status is sent + let ret = list(imap_socket, MbxSelect::All, ListReturn::None)?; + assert!(!ret.contains("* STATUS")); + + Ok(()) + }) + .expect("test fully run"); +} diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs index 147c8f0..a10d4e0 100644 --- a/tests/common/fragments.rs +++ b/tests/common/fragments.rs @@ -38,6 +38,7 @@ pub enum Extension { LiteralPlus, Idle, UidPlus, + ListStatus, } pub enum Enable { @@ -107,6 +108,15 @@ pub enum StatusKind { HighestModSeq, } +pub enum MbxSelect { + All, +} + +pub enum ListReturn { + None, + StatusMessagesUnseen, +} + pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { imap.write(&b"5 capability\r\n"[..])?; @@ -118,6 +128,7 @@ pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { Extension::LiteralPlus => Some("LITERAL+"), Extension::Idle => Some("IDLE"), Extension::UidPlus => Some("UIDPLUS"), + Extension::ListStatus => Some("LIST-STATUS"), }; let mut buffer: [u8; 6000] = [0; 6000]; @@ -169,6 +180,25 @@ pub fn create_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { Ok(()) } +pub fn list(imap: &mut TcpStream, select: MbxSelect, mod_return: ListReturn) -> Result { + let mut buffer: [u8; 6000] = [0; 6000]; + + let select_str = match select { + MbxSelect::All => "%", + }; + + let mod_return_str = match mod_return { + ListReturn::None => "", + ListReturn::StatusMessagesUnseen => " RETURN (STATUS (MESSAGES UNSEEN))", + }; + + imap.write(format!("19 LIST \"\" \"{}\"{}\r\n", select_str, mod_return_str).as_bytes())?; + + let read = read_lines(imap, &mut buffer, Some(&b"19 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + Ok(srv_msg.to_string()) +} + pub fn select(imap: &mut TcpStream, mbx: Mailbox, modifier: SelectMod) -> Result { let mut buffer: [u8; 6000] = [0; 6000]; -- 2.43.4