From 7744625c18aff5990a792bb13a44b60d8c4d4fc5 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 27 Dec 2023 17:37:25 +0100 Subject: [PATCH 01/18] drop old code --- src/future_rest_admin_api.txt | 174 ---------------------------------- 1 file changed, 174 deletions(-) delete mode 100644 src/future_rest_admin_api.txt diff --git a/src/future_rest_admin_api.txt b/src/future_rest_admin_api.txt deleted file mode 100644 index 19ece27..0000000 --- a/src/future_rest_admin_api.txt +++ /dev/null @@ -1,174 +0,0 @@ - Command::FirstLogin { - creds, - user_secrets, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - println!("Please enter your password for key decryption."); - println!("If you are using LDAP login, this must be your LDAP password."); - println!("If you are using the static login provider, enter any password, and this will also become your password for local IMAP access."); - let password = rpassword::prompt_password("Enter password: ")?; - let password_confirm = rpassword::prompt_password("Confirm password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - - CryptoKeys::init(&creds, &user_secrets, &password).await?; - - println!(""); - println!("Cryptographic key setup is complete."); - println!(""); - println!("If you are using the static login provider, add the following section to your .toml configuration file:"); - println!(""); - dump_config(&password, &creds); - } - Command::InitializeLocalKeys { creds } => { - let creds = make_storage_creds(creds); - - println!("Please enter a password for local IMAP access."); - println!("This password is not used for key decryption, your keys will be printed below (do not lose them!)"); - println!( - "If you plan on using LDAP login, stop right here and use `first-login` instead" - ); - let password = rpassword::prompt_password("Enter password: ")?; - let password_confirm = rpassword::prompt_password("Confirm password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - - let master = gen_key(); - let (_, secret) = gen_keypair(); - let keys = CryptoKeys::init_without_password(&creds, &master, &secret).await?; - - println!(""); - println!("Cryptographic key setup is complete."); - println!(""); - println!("Add the following section to your .toml configuration file:"); - println!(""); - dump_config(&password, &creds); - dump_keys(&keys); - } - Command::AddPassword { - creds, - user_secrets, - gen, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - let existing_password = - rpassword::prompt_password("Enter existing password to decrypt keys: ")?; - let new_password = if gen { - let password = base64::encode_config( - &u128::to_be_bytes(thread_rng().gen())[..10], - base64::URL_SAFE_NO_PAD, - ); - println!("Your new password: {}", password); - println!("Keep it safe!"); - password - } else { - let password = rpassword::prompt_password("Enter new password: ")?; - let password_confirm = rpassword::prompt_password("Confirm new password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - password - }; - - let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; - keys.add_password(&creds, &user_secrets, &new_password) - .await?; - println!(""); - println!("New password added successfully."); - } - Command::DeletePassword { - creds, - user_secrets, - allow_delete_all, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - let existing_password = rpassword::prompt_password("Enter password to delete: ")?; - - let keys = match allow_delete_all { - true => Some(CryptoKeys::open(&creds, &user_secrets, &existing_password).await?), - false => None, - }; - - CryptoKeys::delete_password(&creds, &existing_password, allow_delete_all).await?; - - println!(""); - println!("Password was deleted successfully."); - - if let Some(keys) = keys { - println!("As a reminder, here are your cryptographic keys:"); - dump_keys(&keys); - } - } - Command::ShowKeys { - creds, - user_secrets, - } => { - let creds = make_storage_creds(creds); - let user_secrets = make_user_secrets(user_secrets); - - let existing_password = rpassword::prompt_password("Enter key decryption password: ")?; - - let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; - dump_keys(&keys); - } - } - - Ok(()) -} - -fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials { - let s3_region = Region { - name: c.region.clone(), - endpoint: c.s3_endpoint, - }; - let k2v_region = Region { - name: c.region, - endpoint: c.k2v_endpoint, - }; - StorageCredentials { - k2v_region, - s3_region, - aws_access_key_id: c.aws_access_key_id, - aws_secret_access_key: c.aws_secret_access_key, - bucket: c.bucket, - } -} - -fn make_user_secrets(c: UserSecretsArgs) -> UserSecrets { - UserSecrets { - user_secret: c.user_secret, - alternate_user_secrets: c - .alternate_user_secrets - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|x| x.to_string()) - .collect(), - } -} - -fn dump_config(password: &str, creds: &StorageCredentials) { - println!("[login_static.users.]"); - println!( - "password = \"{}\"", - hash_password(password).expect("unable to hash password") - ); - println!("aws_access_key_id = \"{}\"", creds.aws_access_key_id); - println!( - "aws_secret_access_key = \"{}\"", - creds.aws_secret_access_key - ); -} - -fn dump_keys(keys: &CryptoKeys) { - println!("master_key = \"{}\"", base64::encode(&keys.master)); - println!("secret_key = \"{}\"", base64::encode(&keys.secret)); -} From ccc9b6abb66ebda0b91b4e21f8ec2fb2e87390f7 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 27 Dec 2023 18:33:06 +0100 Subject: [PATCH 02/18] add a --dev mode --- src/config.rs | 1 + src/login/demo_provider.rs | 48 +++++++++++++++++++ src/login/mod.rs | 1 + src/main.rs | 26 ++++++++-- src/server.rs | 3 +- tests/{ => instrumentation}/README.md | 0 .../{ => instrumentation}/docker-compose.yml | 0 .../docker/cyrus/Dockerfile | 0 .../docker/cyrus/entrypoint.sh | 0 .../docker/maddy/Dockerfile | 0 .../docker/maddy/entrypoint.sh | 0 tests/{ => instrumentation}/inject_emails.sh | 0 .../rm-mail-parser-expected-struct.py | 0 tests/{ => instrumentation}/send-to-imap.py | 0 tests/{ => instrumentation}/unix2dos.py | 0 15 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/login/demo_provider.rs rename tests/{ => instrumentation}/README.md (100%) rename tests/{ => instrumentation}/docker-compose.yml (100%) rename tests/{ => instrumentation}/docker/cyrus/Dockerfile (100%) rename tests/{ => instrumentation}/docker/cyrus/entrypoint.sh (100%) rename tests/{ => instrumentation}/docker/maddy/Dockerfile (100%) rename tests/{ => instrumentation}/docker/maddy/entrypoint.sh (100%) rename tests/{ => instrumentation}/inject_emails.sh (100%) rename tests/{ => instrumentation}/rm-mail-parser-expected-struct.py (100%) rename tests/{ => instrumentation}/send-to-imap.py (100%) rename tests/{ => instrumentation}/unix2dos.py (100%) diff --git a/src/config.rs b/src/config.rs index 1438910..b9c1f09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,6 +26,7 @@ pub struct ProviderConfig { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "user_driver")] pub enum UserManagement { + Demo, Static(LoginStaticConfig), Ldap(LoginLdapConfig), } diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs new file mode 100644 index 0000000..0efb37c --- /dev/null +++ b/src/login/demo_provider.rs @@ -0,0 +1,48 @@ +use crate::login::*; +use crate::storage::*; + +pub struct DemoLoginProvider{ + keys: CryptoKeys, + in_memory_store: in_memory::MemDb, +} + +impl DemoLoginProvider { + pub fn new() -> Self { + Self { + keys: CryptoKeys::init(), + in_memory_store: in_memory::MemDb::new(), + } + } +} + +#[async_trait] +impl LoginProvider for DemoLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result { + tracing::debug!(user=%username, "login"); + + if username != "alice" { + bail!("user does not exist"); + } + + if password != "hunter2" { + bail!("wrong password"); + } + + let storage = self.in_memory_store.builder("alice").await; + let keys = self.keys.clone(); + + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result { + tracing::debug!(user=%email, "public_login"); + if email != "alice@example.tld" { + bail!("invalid email address"); + } + + let storage = self.in_memory_store.builder("alice").await; + let public_key = self.keys.public.clone(); + + Ok(PublicCredentials { storage, public_key }) + } +} diff --git a/src/login/mod.rs b/src/login/mod.rs index 2926738..6f2ca31 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,5 +1,6 @@ pub mod ldap_provider; pub mod static_provider; +pub mod demo_provider; use base64::Engine; use std::sync::Arc; diff --git a/src/main.rs b/src/main.rs index 3221c2e..3baa8e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,12 @@ struct Args { #[clap(subcommand)] command: Command, + /// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION + #[clap(long)] + dev: bool, + #[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")] + /// Path to the main Aerogramme configuration file config_file: PathBuf, } @@ -158,7 +163,22 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let any_config = read_config(args.config_file)?; + let any_config = if args.dev { + use std::net::*; + AnyConfig::Provider(ProviderConfig { + pid: None, + imap: ImapConfig { + bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143), + }, + lmtp: LmtpConfig { + bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025), + hostname: "example.tld".to_string(), + }, + users: UserManagement::Demo, + }) + } else { + read_config(args.config_file)? + }; match (&args.command, any_config) { (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand { @@ -184,8 +204,8 @@ async fn main() -> Result<()> { ProviderCommand::Account(cmd) => { let user_file = match config.users { UserManagement::Static(conf) => conf.user_list, - UserManagement::Ldap(_) => { - panic!("LDAP account management is not supported from Aerogramme.") + _ => { + panic!("Only static account management is supported from Aerogramme.") } }; account_management(&args.command, cmd, user_file)?; diff --git a/src/server.rs b/src/server.rs index 28e0b27..1b8677b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,7 +11,7 @@ use crate::config::*; use crate::imap; use crate::lmtp::*; use crate::login::ArcLoginProvider; -use crate::login::{ldap_provider::*, static_provider::*}; +use crate::login::{ldap_provider::*, static_provider::*, demo_provider::*}; pub struct Server { lmtp_server: Option>, @@ -36,6 +36,7 @@ impl Server { pub async fn from_provider_config(config: ProviderConfig) -> Result { tracing::info!("Init as provider"); let login: ArcLoginProvider = match config.users { + UserManagement::Demo => Arc::new(DemoLoginProvider::new()), UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?), UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), }; diff --git a/tests/README.md b/tests/instrumentation/README.md similarity index 100% rename from tests/README.md rename to tests/instrumentation/README.md diff --git a/tests/docker-compose.yml b/tests/instrumentation/docker-compose.yml similarity index 100% rename from tests/docker-compose.yml rename to tests/instrumentation/docker-compose.yml diff --git a/tests/docker/cyrus/Dockerfile b/tests/instrumentation/docker/cyrus/Dockerfile similarity index 100% rename from tests/docker/cyrus/Dockerfile rename to tests/instrumentation/docker/cyrus/Dockerfile diff --git a/tests/docker/cyrus/entrypoint.sh b/tests/instrumentation/docker/cyrus/entrypoint.sh similarity index 100% rename from tests/docker/cyrus/entrypoint.sh rename to tests/instrumentation/docker/cyrus/entrypoint.sh diff --git a/tests/docker/maddy/Dockerfile b/tests/instrumentation/docker/maddy/Dockerfile similarity index 100% rename from tests/docker/maddy/Dockerfile rename to tests/instrumentation/docker/maddy/Dockerfile diff --git a/tests/docker/maddy/entrypoint.sh b/tests/instrumentation/docker/maddy/entrypoint.sh similarity index 100% rename from tests/docker/maddy/entrypoint.sh rename to tests/instrumentation/docker/maddy/entrypoint.sh diff --git a/tests/inject_emails.sh b/tests/instrumentation/inject_emails.sh similarity index 100% rename from tests/inject_emails.sh rename to tests/instrumentation/inject_emails.sh diff --git a/tests/rm-mail-parser-expected-struct.py b/tests/instrumentation/rm-mail-parser-expected-struct.py similarity index 100% rename from tests/rm-mail-parser-expected-struct.py rename to tests/instrumentation/rm-mail-parser-expected-struct.py diff --git a/tests/send-to-imap.py b/tests/instrumentation/send-to-imap.py similarity index 100% rename from tests/send-to-imap.py rename to tests/instrumentation/send-to-imap.py diff --git a/tests/unix2dos.py b/tests/instrumentation/unix2dos.py similarity index 100% rename from tests/unix2dos.py rename to tests/instrumentation/unix2dos.py From bf9a5c1757908a031da90a771072ba8f35826980 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 28 Dec 2023 16:37:38 +0100 Subject: [PATCH 03/18] add a basic test --- Cargo.lock | 134 +++++++++++++++++++++++++++++------------ Cargo.toml | 6 ++ tests/imap_features.rs | 17 ++++++ 3 files changed, 117 insertions(+), 40 deletions(-) create mode 100644 tests/imap_features.rs diff --git a/Cargo.lock b/Cargo.lock index 7611ef8..77d32db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,7 @@ dependencies = [ "rmp-serde", "rpassword", "serde", + "serial_test", "smtp-message", "smtp-server", "sodiumoxide", @@ -176,7 +177,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tokio", ] @@ -237,7 +238,7 @@ dependencies = [ "polling", "rustix", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] @@ -300,7 +301,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "pin-utils", "slab", "wasm-bindgen-futures", @@ -314,7 +315,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", ] [[package]] @@ -457,7 +458,7 @@ dependencies = [ "bytes", "http", "http-body", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tracing", ] @@ -637,7 +638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9f65000917e3aa94c259d67fe01fa9e4cd456187d026067d642436e6311a81" dependencies = [ "futures-util", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tokio", ] @@ -656,7 +657,7 @@ dependencies = [ "http", "http-body", "md-5", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "sha1", "sha2", "tracing", @@ -688,7 +689,7 @@ dependencies = [ "hyper", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "pin-utils", "tracing", ] @@ -709,7 +710,7 @@ dependencies = [ "http-body", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "pin-utils", "tracing", ] @@ -751,7 +752,7 @@ dependencies = [ "hyper", "hyper-rustls", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "pin-utils", "rustls 0.21.10", "tokio", @@ -768,7 +769,7 @@ dependencies = [ "aws-smithy-types 1.1.1", "bytes", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tokio", "tracing", "zeroize", @@ -801,7 +802,7 @@ dependencies = [ "http-body", "itoa", "num-integer", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "pin-utils", "ryu", "serde", @@ -983,9 +984,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bytes-utils" @@ -1224,6 +1225,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -1496,7 +1510,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "waker-fn", ] @@ -1536,7 +1550,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "pin-utils", "slab", ] @@ -1616,6 +1630,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hdrhistogram" version = "7.5.2" @@ -1681,7 +1701,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", ] [[package]] @@ -1712,8 +1732,8 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.10", - "socket2", + "pin-project-lite 0.2.13", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1813,7 +1833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -2069,9 +2089,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2307,9 +2327,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2345,7 +2365,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "windows-sys", ] @@ -2791,6 +2811,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2941,6 +2986,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "sodiumoxide" version = "0.2.7" @@ -3050,18 +3105,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", @@ -3133,29 +3188,28 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -3190,7 +3244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tokio", ] @@ -3221,7 +3275,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tokio", "tracing", ] @@ -3246,7 +3300,7 @@ dependencies = [ "hdrhistogram", "indexmap", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "rand", "slab", "tokio", @@ -3276,7 +3330,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.13", "tracing-attributes", "tracing-core", ] diff --git a/Cargo.toml b/Cargo.toml index ce8cc8e..11a215d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,12 @@ smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } #k2v-client = { path = "../garage/src/k2v-client" } +#imap-flow = { git = "https://github.com/duesee/imap-flow.git", commit = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" } [dev-dependencies] +serial_test = "*" +[[test]] +name = "imap_features" +path = "tests/imap_features.rs" +harness = false diff --git a/tests/imap_features.rs b/tests/imap_features.rs new file mode 100644 index 0000000..3333b03 --- /dev/null +++ b/tests/imap_features.rs @@ -0,0 +1,17 @@ +use std::process::Command; +use std::{thread, time}; + +static ONE_SEC: time::Duration = time::Duration::from_secs(1); + +fn main() { + let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) + .arg("--dev") + .arg("provider") + .arg("daemon") + .spawn() + .expect("daemon should be started"); + + thread::sleep(ONE_SEC); + + daemon.kill().expect("daemon should be killed"); +} From b49f7e801bef78736e1a33361b883228dd55f134 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 28 Dec 2023 18:18:21 +0100 Subject: [PATCH 04/18] wip testing --- tests/imap_features.rs | 77 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 3333b03..59892b2 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -1,5 +1,8 @@ use std::process::Command; +use std::net::{Shutdown, TcpStream}; use std::{thread, time}; +use anyhow::Result; +use std::io::{Write, Read}; static ONE_SEC: time::Duration = time::Duration::from_secs(1); @@ -11,7 +14,79 @@ fn main() { .spawn() .expect("daemon should be started"); - thread::sleep(ONE_SEC); + let mut max_retry = 10; + let mut imap_socket = loop { + max_retry -= 1; + match (TcpStream::connect("[::1]:1143"), max_retry) { + (Err(e), 0) => panic!("no more retry, last error is: {}", e), + (Err(e), _) => { + println!("unable to connect: {} ; will retry in 1 sec", e); + }, + (Ok(v), _) => break v, + } + thread::sleep(ONE_SEC); + }; + let mut lmtp_socket = TcpStream::connect("[::1]:1025").expect("lmtp socket must be connected"); + + println!("-- ready to test imap features --"); + login(&mut imap_socket).expect("login test"); + select_inbox(&mut imap_socket).expect("select inbox"); + inject_email(&mut lmtp_socket).expect("inject email"); + + println!("-- test teardown --"); + + imap_socket.shutdown(Shutdown::Both).expect("closing imap socket at the end of the test"); + lmtp_socket.shutdown(Shutdown::Both).expect("closing lmtp socket at the end of the test"); daemon.kill().expect("daemon should be killed"); } + +fn login(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + let mut nbytes = 0; + loop { + nbytes += imap.read(&mut buffer)?; + if buffer[..nbytes].windows(2).any(|w| w == &b"\r\n"[..]) { + break + } + } + println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); + assert_eq!(&buffer[..4], &b"* OK"[..]); + assert_eq!(&buffer[nbytes-2..nbytes], &b"\r\n"[..]); + + imap.write(&b"10 login alice hunter2\r\n"[..])?; + + let mut nbytes = 0; + loop { + nbytes += imap.read(&mut buffer)?; + if &buffer[nbytes-2..nbytes] == &b"\r\n"[..] { + break + } + } + println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); + assert_eq!(&buffer[..5], &b"10 OK"[..]); + + Ok(()) +} + +fn select_inbox(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + imap.write(&b"20 select inbox\r\n"[..])?; + + let mut nbytes = 0; + loop { + nbytes += imap.read(&mut buffer)?; + if buffer[..nbytes].windows(5).any(|w| w == &b"20 OK"[..]) && &buffer[nbytes-2..nbytes] == &b"\r\n"[..] { + break + } + } + println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); + + Ok(()) +} + +fn inject_email(lmtp: &mut TcpStream) -> Result<()> { + + Ok(()) +} From adb1a3b7c1cb24a773060f5944cdfe1ea7bd5816 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 29 Dec 2023 12:38:42 +0100 Subject: [PATCH 05/18] fix "fetch x rfc822" close #33 --- src/imap/mailbox_view.rs | 2 +- tests/imap_features.rs | 170 +++++++++++++++++++++++++++++++-------- 2 files changed, 138 insertions(+), 34 deletions(-) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 99069e2..f896448 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -174,7 +174,7 @@ impl<'a> MailView<'a> { Ok(MessageAttribute::Rfc822(NString( self.content .as_full()? - .raw_body + .raw_part .clone() .try_into() .ok() diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 59892b2..3e4d1a9 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -1,10 +1,54 @@ use std::process::Command; use std::net::{Shutdown, TcpStream}; use std::{thread, time}; -use anyhow::Result; +use anyhow::{bail, Result}; use std::io::{Write, Read}; -static ONE_SEC: time::Duration = time::Duration::from_secs(1); +static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); +static EMAIL: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r +From: Bob Robert \r +To: Alice Malice \r +CC: =?ISO-8859-1?Q?Andr=E9?= Pirard \r +Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r + =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r +X-Unknown: something something\r +Bad entry\r + on multiple lines\r +Message-ID: \r +MIME-Version: 1.0\r +Content-Type: multipart/alternative;\r + boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r +Content-Transfer-Encoding: 7bit\r +\r +This is a multi-part message in MIME format.\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956\r +Content-Type: text/plain; charset=utf-8\r +Content-Transfer-Encoding: quoted-printable\r +\r +GZ\r +OoOoO\r +oOoOoOoOo\r +oOoOoOoOoOoOoOoOo\r +oOoOoOoOoOoOoOoOoOoOoOo\r +oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r +OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956\r +Content-Type: text/html; charset=us-ascii\r +\r +
GZ
\r +OoOoO
\r +oOoOoOoOo
\r +oOoOoOoOoOoOoOoOo
\r +oOoOoOoOoOoOoOoOoOoOoOo
\r +oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
\r +OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r +
\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956--\r +"; + fn main() { let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) @@ -14,7 +58,7 @@ fn main() { .spawn() .expect("daemon should be started"); - let mut max_retry = 10; + let mut max_retry = 20; let mut imap_socket = loop { max_retry -= 1; match (TcpStream::connect("[::1]:1143"), max_retry) { @@ -24,7 +68,7 @@ fn main() { }, (Ok(v), _) => break v, } - thread::sleep(ONE_SEC); + thread::sleep(SMALL_DELAY); }; let mut lmtp_socket = TcpStream::connect("[::1]:1025").expect("lmtp socket must be connected"); @@ -32,7 +76,10 @@ fn main() { println!("-- ready to test imap features --"); login(&mut imap_socket).expect("login test"); select_inbox(&mut imap_socket).expect("select inbox"); - inject_email(&mut lmtp_socket).expect("inject email"); + lmtp_handshake(&mut lmtp_socket).expect("handshake lmtp done"); + lmtp_deliver_email(&mut lmtp_socket, EMAIL).expect("mail delivered successfully"); + noop_exists(&mut imap_socket).expect("noop loop must detect a new email"); + fetch_rfc822(&mut imap_socket, EMAIL).expect("fecth rfc822 message"); println!("-- test teardown --"); @@ -43,28 +90,14 @@ fn main() { fn login(imap: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 1500] = [0; 1500]; - let mut nbytes = 0; - loop { - nbytes += imap.read(&mut buffer)?; - if buffer[..nbytes].windows(2).any(|w| w == &b"\r\n"[..]) { - break - } - } - println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); - assert_eq!(&buffer[..4], &b"* OK"[..]); - assert_eq!(&buffer[nbytes-2..nbytes], &b"\r\n"[..]); + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..4], &b"* OK"[..]); imap.write(&b"10 login alice hunter2\r\n"[..])?; - let mut nbytes = 0; - loop { - nbytes += imap.read(&mut buffer)?; - if &buffer[nbytes-2..nbytes] == &b"\r\n"[..] { - break - } - } - println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); - assert_eq!(&buffer[..5], &b"10 OK"[..]); + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"10 OK"[..]); Ok(()) } @@ -73,20 +106,91 @@ fn select_inbox(imap: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 6000] = [0; 6000]; imap.write(&b"20 select inbox\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; + Ok(()) +} + +fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let _read = read_lines(lmtp, &mut buffer, None)?; + assert_eq!(&buffer[..4], &b"220 "[..]); + + lmtp.write(&b"LHLO example.tld\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; + + Ok(()) +} + +fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + lmtp.write(&b"MAIL FROM:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + lmtp.write(&b"RCPT TO:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; + + lmtp.write(&b"DATA\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; + + lmtp.write(email)?; + lmtp.write(&b"\r\n.\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + Ok(()) +} + +fn noop_exists(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + let mut max_retry = 20; + loop { + max_retry -= 1; + imap.write(&b"30 NOOP\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + + match (max_retry, srv_msg.contains("* 1 EXISTS")) { + (_, true) => break, + (0, _) => bail!("no more retry"), + _ => (), + } + + thread::sleep(SMALL_DELAY); + }; + + Ok(()) +} + +fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { + let mut buffer: [u8; 65535] = [0; 65535]; + imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + let orig_email = std::str::from_utf8(ref_mail)?; + assert!(srv_msg.contains(orig_email)); + + Ok(()) +} + + +fn read_lines<'a, F: Read>(reader: &mut F, buffer: &'a mut [u8], stop_marker: Option<&[u8]>) -> Result<&'a [u8]> { let mut nbytes = 0; loop { - nbytes += imap.read(&mut buffer)?; - if buffer[..nbytes].windows(5).any(|w| w == &b"20 OK"[..]) && &buffer[nbytes-2..nbytes] == &b"\r\n"[..] { + nbytes += reader.read(&mut buffer[nbytes..])?; + let pre_condition = match stop_marker { + None => true, + Some(mark) => buffer[..nbytes] + .windows(mark.len()) + .any(|w| w == mark) + }; + if pre_condition && &buffer[nbytes-2..nbytes] == &b"\r\n"[..] { break } } println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); - - Ok(()) -} - -fn inject_email(lmtp: &mut TcpStream) -> Result<()> { - - Ok(()) + Ok(&buffer[..nbytes]) } From 771c4eac799ec3d9f1e9c41ab1fdc75c1bcb4868 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 29 Dec 2023 17:16:41 +0100 Subject: [PATCH 06/18] covering imap commands --- src/bayou.rs | 5 +- src/imap/command/anonymous.rs | 5 +- src/login/demo_provider.rs | 13 ++- src/login/mod.rs | 2 +- src/server.rs | 2 +- tests/imap_features.rs | 187 +++++++++++++++++++++++++++++----- 6 files changed, 178 insertions(+), 36 deletions(-) diff --git a/src/bayou.rs b/src/bayou.rs index 7253a30..c6a7ac0 100644 --- a/src/bayou.rs +++ b/src/bayou.rs @@ -450,10 +450,7 @@ impl K2vWatch { ) { let mut row = match Weak::upgrade(&self_weak) { Some(this) => this.target.clone(), - None => { - error!("can't start loop"); - return; - } + None => return, }; while let Some(this) = Weak::upgrade(&self_weak) { diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index d258bd3..6ba19cf 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -21,7 +21,10 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Tran CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, CommandBody::Login { username, password } => ctx.login(username, password).await, - _ => Ok((Response::no("Command unavailable")?, flow::Transition::None)), + cmd => { + tracing::warn!("Unknown command {:?}", cmd); + Ok((Response::no("Command unavailable")?, flow::Transition::None)) + } } } diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs index 0efb37c..11c7d54 100644 --- a/src/login/demo_provider.rs +++ b/src/login/demo_provider.rs @@ -1,14 +1,14 @@ use crate::login::*; use crate::storage::*; -pub struct DemoLoginProvider{ +pub struct DemoLoginProvider { keys: CryptoKeys, in_memory_store: in_memory::MemDb, } impl DemoLoginProvider { pub fn new() -> Self { - Self { + Self { keys: CryptoKeys::init(), in_memory_store: in_memory::MemDb::new(), } @@ -26,8 +26,8 @@ impl LoginProvider for DemoLoginProvider { if password != "hunter2" { bail!("wrong password"); - } - + } + let storage = self.in_memory_store.builder("alice").await; let keys = self.keys.clone(); @@ -43,6 +43,9 @@ impl LoginProvider for DemoLoginProvider { let storage = self.in_memory_store.builder("alice").await; let public_key = self.keys.public.clone(); - Ok(PublicCredentials { storage, public_key }) + Ok(PublicCredentials { + storage, + public_key, + }) } } diff --git a/src/login/mod.rs b/src/login/mod.rs index 6f2ca31..4a1dee1 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,6 +1,6 @@ +pub mod demo_provider; pub mod ldap_provider; pub mod static_provider; -pub mod demo_provider; use base64::Engine; use std::sync::Arc; diff --git a/src/server.rs b/src/server.rs index 1b8677b..8bfde98 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,7 +11,7 @@ use crate::config::*; use crate::imap; use crate::lmtp::*; use crate::login::ArcLoginProvider; -use crate::login::{ldap_provider::*, static_provider::*, demo_provider::*}; +use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*}; pub struct Server { lmtp_server: Option>, diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 3e4d1a9..4fee119 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -1,8 +1,8 @@ -use std::process::Command; +use anyhow::{bail, Context, Result}; +use std::io::{Read, Write}; use std::net::{Shutdown, TcpStream}; +use std::process::Command; use std::{thread, time}; -use anyhow::{bail, Result}; -use std::io::{Write, Read}; static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); static EMAIL: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r @@ -49,7 +49,6 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r --b1_e376dc71bafc953c0b0fdeb9983a9956--\r "; - fn main() { let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) .arg("--dev") @@ -65,7 +64,7 @@ fn main() { (Err(e), 0) => panic!("no more retry, last error is: {}", e), (Err(e), _) => { println!("unable to connect: {} ; will retry in 1 sec", e); - }, + } (Ok(v), _) => break v, } thread::sleep(SMALL_DELAY); @@ -74,26 +73,66 @@ fn main() { let mut lmtp_socket = TcpStream::connect("[::1]:1025").expect("lmtp socket must be connected"); println!("-- ready to test imap features --"); - login(&mut imap_socket).expect("login test"); - select_inbox(&mut imap_socket).expect("select inbox"); - lmtp_handshake(&mut lmtp_socket).expect("handshake lmtp done"); - lmtp_deliver_email(&mut lmtp_socket, EMAIL).expect("mail delivered successfully"); - noop_exists(&mut imap_socket).expect("noop loop must detect a new email"); - fetch_rfc822(&mut imap_socket, EMAIL).expect("fecth rfc822 message"); - + let result = generic_test(&mut imap_socket, &mut lmtp_socket); println!("-- test teardown --"); - imap_socket.shutdown(Shutdown::Both).expect("closing imap socket at the end of the test"); - lmtp_socket.shutdown(Shutdown::Both).expect("closing lmtp socket at the end of the test"); + imap_socket + .shutdown(Shutdown::Both) + .expect("closing imap socket at the end of the test"); + lmtp_socket + .shutdown(Shutdown::Both) + .expect("closing lmtp socket at the end of the test"); daemon.kill().expect("daemon should be killed"); + + result.expect("all tests passed"); } -fn login(imap: &mut TcpStream) -> Result<()> { +fn generic_test(imap_socket: &mut TcpStream, lmtp_socket: &mut TcpStream) -> Result<()> { + connect(imap_socket).context("server says hello")?; + capability(imap_socket).context("check server capabilities")?; + login(imap_socket).context("login test")?; + create_mailbox(imap_socket).context("created mailbox archive")?; + // UNSUBSCRIBE IS NOT IMPLEMENTED YET + //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; + select_inbox(imap_socket).context("select inbox")?; + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; + noop_exists(imap_socket).context("noop loop must detect a new email")?; + fetch_rfc822(imap_socket, EMAIL).context("fetch rfc822 message")?; + copy_email(imap_socket).context("copy message to the archive mailbox")?; + // SEARCH IS NOT IMPLEMENTED YET + //search(imap_socket).expect("search should return something"); + add_flags_email(imap_socket).context("should add delete and important flags to the email")?; + expunge(imap_socket).context("expunge emails")?; + rename_mailbox(imap_socket).context("archive mailbox is renamed my-archives")?; + delete_mailbox(imap_socket).context("my-archives mailbox is deleted")?; + Ok(()) +} + +fn connect(imap: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 1500] = [0; 1500]; let read = read_lines(imap, &mut buffer, None)?; assert_eq!(&read[..4], &b"* OK"[..]); + Ok(()) +} + +fn capability(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"5 capability\r\n"[..])?; + + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("IMAP4REV1")); + assert!(srv_msg.contains("IDLE")); + + Ok(()) +} + +fn login(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + imap.write(&b"10 login alice hunter2\r\n"[..])?; let read = read_lines(imap, &mut buffer, None)?; @@ -102,6 +141,39 @@ fn login(imap: &mut TcpStream) -> Result<()> { Ok(()) } +fn create_mailbox(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + imap.write(&b"15 create archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..12], &b"15 OK CREATE"[..]); + + Ok(()) +} + +#[allow(dead_code)] +fn unsubscribe_mailbox(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + imap.write(&b"16 lsub \"\" *\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"16 OK LSUB"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains(" INBOX\r\n")); + assert!(srv_msg.contains(" archive\r\n")); + + imap.write(&b"17 unsubscribe archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"17 OK"[..]); + + imap.write(&b"18 lsub \"\" *\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"18 OK LSUB"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains(" INBOX\r\n")); + assert!(!srv_msg.contains(" archive\r\n")); + + Ok(()) +} + fn select_inbox(imap: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 6000] = [0; 6000]; @@ -144,7 +216,7 @@ fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { fn noop_exists(imap: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 6000] = [0; 6000]; - + let mut max_retry = 20; loop { max_retry -= 1; @@ -157,9 +229,9 @@ fn noop_exists(imap: &mut TcpStream) -> Result<()> { (0, _) => bail!("no more retry"), _ => (), } - + thread::sleep(SMALL_DELAY); - }; + } Ok(()) } @@ -176,19 +248,86 @@ fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { Ok(()) } +fn copy_email(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 65535] = [0; 65535]; + imap.write(&b"45 copy 1 archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"45 OK"[..]); -fn read_lines<'a, F: Read>(reader: &mut F, buffer: &'a mut [u8], stop_marker: Option<&[u8]>) -> Result<&'a [u8]> { + Ok(()) +} + +fn add_flags_email(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"50 store 1 +FLAGS (\\Deleted \\Important)\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; + + Ok(()) +} + +#[allow(dead_code)] +/// Not yet implemented +fn search(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"55 OK SEARCH"[..]))?; + Ok(()) +} + +fn expunge(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"60 expunge\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"60 OK EXPUNGE"[..]))?; + + Ok(()) +} + +fn rename_mailbox(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"70 rename archive my-archives\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"70 OK"[..]); + + imap.write(&b"71 list \"\" *\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"71 OK LIST"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(!srv_msg.contains(" archive\r\n")); + assert!(srv_msg.contains(" INBOX\r\n")); + assert!(srv_msg.contains(" my-archives\r\n")); + + Ok(()) +} + +fn delete_mailbox(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"80 delete my-archives\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"80 OK"[..]); + + imap.write(&b"81 list \"\" *\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"81 OK LIST"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(!srv_msg.contains(" archive\r\n")); + assert!(!srv_msg.contains(" my-archives\r\n")); + assert!(srv_msg.contains(" INBOX\r\n")); + + Ok(()) +} + +fn read_lines<'a, F: Read>( + reader: &mut F, + buffer: &'a mut [u8], + stop_marker: Option<&[u8]>, +) -> Result<&'a [u8]> { let mut nbytes = 0; loop { nbytes += reader.read(&mut buffer[nbytes..])?; let pre_condition = match stop_marker { None => true, - Some(mark) => buffer[..nbytes] - .windows(mark.len()) - .any(|w| w == mark) + Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark), }; - if pre_condition && &buffer[nbytes-2..nbytes] == &b"\r\n"[..] { - break + if pre_condition && &buffer[nbytes - 2..nbytes] == &b"\r\n"[..] { + break; } } println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); From 608dab8e5d7eeed1f7309ab8c5b73c4940a177d0 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 30 Dec 2023 09:29:21 +0100 Subject: [PATCH 07/18] WIP implem status --- tests/imap_features.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 4fee119..b028756 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -95,6 +95,9 @@ fn generic_test(imap_socket: &mut TcpStream, lmtp_socket: &mut TcpStream) -> Res // UNSUBSCRIBE IS NOT IMPLEMENTED YET //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; select_inbox(imap_socket).context("select inbox")?; + // CHECK IS NOT IMPLEMENTED YET + //check(...) + status_mailbox(imap_socket).context("status inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; noop_exists(imap_socket).context("noop loop must detect a new email")?; @@ -183,6 +186,11 @@ fn select_inbox(imap: &mut TcpStream) -> Result<()> { Ok(()) } +fn status_mailbox(imap: &mut TcpStream) -> Result<()> { + + Ok(()) +} + fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 1500] = [0; 1500]; From 3004c6982268726fe4b66981287dcbe8bf62aae1 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 30 Dec 2023 10:35:01 +0100 Subject: [PATCH 08/18] check status --- Cargo.lock | 47 +----------------------------------------- Cargo.toml | 1 - tests/imap_features.rs | 5 ++++- 3 files changed, 5 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77d32db..ceac5fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,6 @@ dependencies = [ "rmp-serde", "rpassword", "serde", - "serial_test", "smtp-message", "smtp-server", "sodiumoxide", @@ -1225,19 +1224,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.3", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "data-encoding" version = "2.4.0" @@ -1630,12 +1616,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - [[package]] name = "hdrhistogram" version = "7.5.2" @@ -1833,7 +1813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.12.3", + "hashbrown", ] [[package]] @@ -2811,31 +2791,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serial_test" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" -dependencies = [ - "dashmap", - "futures", - "lazy_static", - "log", - "parking_lot", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 11a215d..0c48ff8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/ #imap-flow = { git = "https://github.com/duesee/imap-flow.git", commit = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" } [dev-dependencies] -serial_test = "*" [[test]] name = "imap_features" diff --git a/tests/imap_features.rs b/tests/imap_features.rs index b028756..844b176 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -97,7 +97,7 @@ fn generic_test(imap_socket: &mut TcpStream, lmtp_socket: &mut TcpStream) -> Res select_inbox(imap_socket).context("select inbox")?; // CHECK IS NOT IMPLEMENTED YET //check(...) - status_mailbox(imap_socket).context("status inbox")?; + status_mailbox(imap_socket).context("status of archive from inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; noop_exists(imap_socket).context("noop loop must detect a new email")?; @@ -187,6 +187,9 @@ fn select_inbox(imap: &mut TcpStream) -> Result<()> { } fn status_mailbox(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"25 STATUS archive (UIDNEXT MESSAGES)\r\n"[..])?; + let mut buffer: [u8; 6000] = [0; 6000]; + let _read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?; Ok(()) } From 6e20778f74a89d4b7a9b2c9cfca5bb2907bb0d22 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 30 Dec 2023 11:23:10 +0100 Subject: [PATCH 09/18] broken build, reworked dependencies --- Cargo.lock | 1296 ++++++++++++++++++++++++++-------------------------- Cargo.toml | 78 ++-- 2 files changed, 691 insertions(+), 683 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ceac5fe..a6a01b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "abnf-core" -version = "0.4.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871a574ed52e84ec15e6266d57d477e3e5c396cd86f9b05f2cb629a2c5af2eec" +checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea" dependencies = [ - "nom 6.1.2", + "nom 7.1.3", ] [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -36,8 +36,7 @@ dependencies = [ "aws-config", "aws-sdk-s3", "backtrace", - "base64 0.21.2", - "boitalettres", + "base64 0.21.5", "chrono", "clap", "duplexify", @@ -47,6 +46,7 @@ dependencies = [ "hyper-rustls", "im", "imap-codec", + "imap-flow", "itertools", "k2v-client", "lazy_static", @@ -63,7 +63,6 @@ dependencies = [ "tokio", "tokio-util", "toml", - "tower", "tracing", "tracing-subscriber", "zstd", @@ -95,9 +94,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" [[package]] name = "argon2" @@ -130,7 +129,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.23", + "time", ] [[package]] @@ -163,34 +162,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] -name = "async-compat" -version = "0.2.1" +name = "async-channel" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b48b4ff0c2026db683dea961cd8ea874737f56cffca86fa84415eaddc51c00d" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ + "concurrent-queue", + "event-listener 4.0.1", + "event-listener-strategy", "futures-core", - "futures-io", - "once_cell", "pin-project-lite 0.2.13", - "tokio", ] [[package]] name = "async-executor" -version = "1.5.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ - "async-lock", + "async-lock 3.2.0", "async-task", "concurrent-queue", - "fastrand 1.9.0", - "futures-lite", + "fastrand 2.0.1", + "futures-lite 2.1.0", "slab", ] @@ -200,24 +199,24 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" dependencies = [ - "async-lock", + "async-lock 2.8.0", "autocfg", "blocking", - "futures-lite", + "futures-lite 1.13.0", ] [[package]] name = "async-global-executor" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel", + "async-channel 2.1.1", "async-executor", - "async-io", - "async-lock", + "async-io 2.2.2", + "async-lock 3.2.0", "blocking", - "futures-lite", + "futures-lite 2.1.0", "once_cell", ] @@ -227,57 +226,103 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ - "async-lock", + "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", - "futures-lite", + "futures-lite 1.13.0", "log", "parking", - "polling", - "rustix", + "polling 2.8.0", + "rustix 0.37.27", "slab", - "socket2 0.4.9", + "socket2 0.4.10", "waker-fn", ] [[package]] -name = "async-lock" -version = "2.7.0" +name = "async-io" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" dependencies = [ - "event-listener", + "async-lock 3.2.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.1.0", + "parking", + "polling 3.3.1", + "rustix 0.38.28", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.1", + "event-listener-strategy", + "pin-project-lite 0.2.13", ] [[package]] name = "async-net" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" +checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" dependencies = [ - "async-io", - "autocfg", + "async-io 1.13.0", "blocking", - "futures-lite", + "futures-lite 1.13.0", ] [[package]] name = "async-process" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-io", - "async-lock", - "autocfg", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", "blocking", "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "signal-hook", - "windows-sys", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.28", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io 2.2.2", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.28", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", ] [[package]] @@ -286,15 +331,15 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ - "async-channel", + "async-channel 1.9.0", "async-global-executor", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite", + "futures-lite 1.13.0", "gloo-timers", "kv-log-macro", "log", @@ -306,50 +351,28 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite 0.2.13", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] - [[package]] name = "async-task" -version = "4.4.0" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atty" @@ -427,7 +450,7 @@ dependencies = [ "http", "hyper", "ring 0.17.7", - "time 0.3.23", + "time", "tokio", "tracing", "zeroize", @@ -485,9 +508,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f89aed8bb54ff816b860cda5d6f010cf54b7b64a46bb66e3edd1063ac5f36e" +checksum = "21392b29994de019a7059af5eab144ea49d572dd52863d8e10537267f59f998c" dependencies = [ "aws-credential-types", "aws-http", @@ -515,9 +538,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8539671a6e0ca087464f784576b42796ed934a566b3c1263dcf2a88494d067" +checksum = "da9d9a8ac4cdb8df39f9777fd41e15a9ae0d0b622b00909ae0322b4d2f9e6ac8" dependencies = [ "aws-credential-types", "aws-http", @@ -538,9 +561,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f96ac3757df21b29102edd909884ceb8d118b96b5e45546b8bb59b8a9a8eb85a" +checksum = "56ba4a42aa91acecd5ca43b330b5c8eb7f8808d720b6a6f796a35faa302fc73d" dependencies = [ "aws-credential-types", "aws-http", @@ -561,9 +584,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe238802a9ad92721c23fd0b1d52284ec9e3565d6d0a12243608833e2655abb" +checksum = "8e3c7c3dcec7cccd24a13953eedf0f2964c2d728d22112744274cf0098ad2e35" dependencies = [ "aws-credential-types", "aws-http", @@ -598,7 +621,7 @@ dependencies = [ "percent-encoding", "regex", "sha2", - "time 0.3.23", + "time", "tracing", ] @@ -625,7 +648,7 @@ dependencies = [ "ring 0.17.7", "sha2", "subtle", - "time 0.3.23", + "time", "tracing", "zeroize", ] @@ -784,7 +807,7 @@ dependencies = [ "itoa", "num-integer", "ryu", - "time 0.3.23", + "time", ] [[package]] @@ -805,7 +828,7 @@ dependencies = [ "pin-utils", "ryu", "serde", - "time 0.3.23", + "time", "tokio", "tokio-util", ] @@ -836,9 +859,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -863,9 +886,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64-simd" @@ -879,9 +902,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.0.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" @@ -936,50 +959,51 @@ dependencies = [ [[package]] name = "blocking" -version = "1.3.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel", - "async-lock", + "async-channel 2.1.1", + "async-lock 3.2.0", "async-task", - "atomic-waker", - "fastrand 1.9.0", - "futures-lite", - "log", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.1.0", + "piper", + "tracing", ] [[package]] -name = "boitalettres" -version = "0.1.0" -source = "git+https://git.deuxfleurs.fr/quentin/boitalettres.git?branch=expose-mydatetime#874fa91186989ced7ebac270f18b64e47ee4f98a" +name = "bounded-static" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2325bd33fa7e3018e7e37f5b0591ba009124963b5a3f8b7cae6d0a8c1028ed4" dependencies = [ - "async-compat", - "async-stream", - "bytes", - "futures", - "imap-codec", - "miette", - "pin-project", - "thiserror", - "tokio", - "tokio-tower", - "tower", - "tracing", - "tracing-futures", + "bounded-static-derive", +] + +[[package]] +name = "bounded-static-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10dd247355bf631d98d2753d87ae62c84c8dcb996ad9b24a4168e0aec29bd6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.43", ] [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -1015,17 +1039,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] @@ -1038,7 +1061,7 @@ dependencies = [ "bitflags 1.3.2", "clap_derive", "clap_lex", - "indexmap", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", @@ -1069,9 +1092,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -1084,9 +1107,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -1094,15 +1117,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -1125,69 +1148,11 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" -dependencies = [ - "cfg-if", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ "cfg-if", ] @@ -1226,9 +1191,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "der" @@ -1254,6 +1219,15 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_utils" version = "0.11.2" @@ -1284,7 +1258,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] @@ -1349,7 +1323,7 @@ name = "eml-codec" version = "0.1.2" source = "git+https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git?branch=main#a7bd3c475a58e42b86c163ec075ce01ddae7e60a" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "chrono", "encoding_rs", "nom 7.1.3", @@ -1357,32 +1331,27 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] -name = "errno" -version = "0.3.1" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "errno" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -1391,6 +1360,38 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "event-listener" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f2cdcf274580f2d63697192d744727b3198894b1bf02923643bf59e2c26712" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.1", + "pin-project-lite 0.2.13", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1424,9 +1425,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1439,9 +1440,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1454,9 +1455,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1464,15 +1465,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1481,9 +1482,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1501,33 +1502,46 @@ dependencies = [ ] [[package]] -name = "futures-macro" -version = "0.3.28" +name = "futures-lite" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1553,20 +1567,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gloo-timers" @@ -1593,9 +1607,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1603,7 +1617,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1617,14 +1631,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "hdrhistogram" -version = "7.5.2" +name = "hashbrown" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" -dependencies = [ - "byteorder", - "num-traits", -] +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -1643,9 +1653,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1664,9 +1674,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1675,9 +1685,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1692,15 +1702,15 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1713,7 +1723,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite 0.2.13", - "socket2 0.4.9", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1722,9 +1732,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", @@ -1738,16 +1748,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -1772,9 +1782,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1796,14 +1806,40 @@ dependencies = [ [[package]] name = "imap-codec" -version = "0.5.0" -source = "git+https://github.com/superboum/imap-codec.git?branch=v0.5.x#09f18cff93b7e17e5b84ab1fbf06b8d13b8e9e2d" +version = "1.0.0" +source = "git+https://github.com/duesee/imap-codec?branch=v2#1f490146bb6197eee6032205e3aa7f297efd9b39" dependencies = [ "abnf-core", - "base64 0.13.1", + "base64 0.21.5", + "bounded-static", "chrono", - "nom 6.1.2", - "rand", + "imap-types", + "log", + "nom 7.1.3", + "thiserror", +] + +[[package]] +name = "imap-flow" +version = "0.1.0" +source = "git+https://github.com/duesee/imap-flow.git?rev=e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90#e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" +dependencies = [ + "bounded-static", + "bytes", + "imap-codec", + "thiserror", + "tokio", +] + +[[package]] +name = "imap-types" +version = "1.0.0" +source = "git+https://github.com/duesee/imap-codec?branch=v2#1f490146bb6197eee6032205e3aa7f297efd9b39" +dependencies = [ + "base64 0.21.5", + "bounded-static", + "chrono", + "thiserror", ] [[package]] @@ -1813,7 +1849,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", ] [[package]] @@ -1831,9 +1877,9 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1847,24 +1893,24 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -1875,7 +1921,7 @@ version = "0.0.4" source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?tag=v0.9.0#952c9570c494468643353ee1ae9052b510353665" dependencies = [ "aws-sigv4 0.55.3", - "base64 0.21.2", + "base64 0.21.5", "hex", "http", "hyper", @@ -1931,7 +1977,7 @@ dependencies = [ "nom 2.2.1", "percent-encoding", "ring 0.16.20", - "rustls 0.20.8", + "rustls 0.20.9", "rustls-native-certs", "thiserror", "tokio", @@ -1980,20 +2026,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] -name = "lock_api" -version = "0.4.10" +name = "linux-raw-sys" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "value-bag", ] @@ -2016,41 +2058,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miette" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" -dependencies = [ - "miette-derive", - "once_cell", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", -] +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "minimal-lexical" @@ -2074,8 +2084,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "wasi", + "windows-sys 0.48.0", ] [[package]] @@ -2130,9 +2140,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -2151,9 +2161,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -2164,15 +2174,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", ] [[package]] name = "object" -version = "0.31.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -2188,9 +2198,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl-probe" @@ -2200,9 +2210,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.5.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "outref" @@ -2229,32 +2239,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "password-hash" @@ -2275,28 +2262,28 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] @@ -2317,6 +2304,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -2329,9 +2327,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "polling" @@ -2346,9 +2344,29 @@ dependencies = [ "libc", "log", "pin-project-lite 0.2.13", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "polling" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite 0.2.13", + "rustix 0.38.28", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2381,18 +2399,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2442,25 +2460,16 @@ dependencies = [ "rand_core", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" -version = "1.9.4" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.7", - "regex-syntax 0.7.5", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -2474,13 +2483,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", ] [[package]] @@ -2497,9 +2506,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rfc6979" @@ -2538,7 +2547,7 @@ dependencies = [ "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2565,23 +2574,23 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.2.0" +version = "7.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" dependencies = [ "libc", "rtoolbox", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "rtoolbox" -version = "0.0.1" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -2610,23 +2619,36 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", - "windows-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" dependencies = [ "log", "ring 0.16.20", @@ -2660,11 +2682,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", ] [[package]] @@ -2679,9 +2701,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -2694,27 +2716,21 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -2756,35 +2772,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -2815,23 +2831,13 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2863,18 +2869,18 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smol" @@ -2882,15 +2888,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" dependencies = [ - "async-channel", + "async-channel 1.9.0", "async-executor", "async-fs", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "async-net", "async-process", "blocking", - "futures-lite", + "futures-lite 1.13.0", ] [[package]] @@ -2933,9 +2939,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -2948,7 +2954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3016,9 +3022,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -3045,9 +3051,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -3075,7 +3081,7 @@ checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] @@ -3090,22 +3096,13 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ + "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -3113,15 +3110,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -3152,12 +3149,11 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite 0.2.13", "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3168,7 +3164,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] @@ -3177,7 +3173,7 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls 0.20.8", + "rustls 0.20.9", "tokio", "webpki", ] @@ -3203,28 +3199,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tower" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4322b6e2ebfd3be4082c16df4341505ef333683158b55f22afaf3f61565d728" -dependencies = [ - "crossbeam", - "futures-core", - "futures-sink", - "futures-util", - "pin-project", - "tokio", - "tower", - "tower-service", - "tracing", -] - [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3244,33 +3223,6 @@ dependencies = [ "serde", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "hdrhistogram", - "indexmap", - "pin-project", - "pin-project-lite 0.2.13", - "rand", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - [[package]] name = "tower-service" version = "0.3.2" @@ -3279,12 +3231,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", - "log", "pin-project-lite 0.2.13", "tracing-attributes", "tracing-core", @@ -3292,51 +3242,41 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -3348,27 +3288,27 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3379,12 +3319,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "unicode-xid" version = "0.2.4" @@ -3405,12 +3339,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna 0.5.0", "percent-encoding", ] @@ -3434,9 +3368,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" [[package]] name = "version_check" @@ -3452,15 +3386,15 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -3475,12 +3409,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3489,9 +3417,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3499,24 +3427,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -3526,9 +3454,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3536,28 +3464,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.43", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3565,12 +3493,12 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -3591,9 +3519,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -3605,12 +3533,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -3619,65 +3547,131 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "wyz" @@ -3700,7 +3694,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.23", + "time", ] [[package]] @@ -3711,9 +3705,9 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 0c48ff8..2a55524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,52 +7,66 @@ license = "AGPL-3.0" description = "Encrypted mail storage over Garage" [dependencies] -aws-config = { version = "1.1.1", features = ["behavior-version-latest"] } -aws-sdk-s3 = "1.9.0" -anyhow = "1.0.28" -argon2 = "0.5" -async-trait = "0.1" -backtrace = "0.3" -base64 = "0.21" -clap = { version = "3.1.18", features = ["derive", "env"] } -duplexify = "1.1.0" -eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" } -hex = "0.4" -futures = "0.3" -im = "15" -itertools = "0.10" -lazy_static = "1.4" -ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] } -log = "0.4" -hyper-rustls = { version = "0.24", features = ["http2"] } -nix = { version = "0.27", features = ["signal"] } -serde = "1.0.137" -rand = "0.8.5" -rmp-serde = "0.15" -rpassword = "7.0" -sodiumoxide = "0.2" +# async runtime tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } tokio-util = { version = "0.7", features = [ "compat" ] } -toml = "0.5" -zstd = { version = "0.9", default-features = false } +futures = "0.3" +# debug +log = "0.4" +backtrace = "0.3" tracing-subscriber = "0.3" tracing = "0.1" -tower = "0.4" -imap-codec = { git = "https://github.com/superboum/imap-codec.git", branch = "v0.5.x" } +# language extensions +lazy_static = "1.4" +duplexify = "1.1.0" +im = "15" +anyhow = "1.0.28" +async-trait = "0.1" +itertools = "0.10" chrono = { version = "0.4", default-features = false, features = ["alloc"] } +# process related +nix = { version = "0.27", features = ["signal"] } +clap = { version = "3.1.18", features = ["derive", "env"] } + +# serialization +serde = "1.0.137" +rmp-serde = "0.15" +toml = "0.5" +base64 = "0.21" +hex = "0.4" +zstd = { version = "0.9", default-features = false } + +# cryptography & security +sodiumoxide = "0.2" +argon2 = "0.5" +rand = "0.8.5" +hyper-rustls = { version = "0.24", features = ["http2"] } +rpassword = "7.0" + +# login +ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] } + +# storage k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", tag = "v0.9.0" } -boitalettres = { git = "https://git.deuxfleurs.fr/quentin/boitalettres.git", branch = "expose-mydatetime" } +aws-config = { version = "1.1.1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.9.0" + +# email protocols +eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" } smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } - -#k2v-client = { path = "../garage/src/k2v-client" } -#imap-flow = { git = "https://github.com/duesee/imap-flow.git", commit = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" } +imap-codec = { version = "1.0.0", features = ["quirk_crlf_relaxed", "bounded-static"] } +imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" } [dev-dependencies] +[patch.crates-io] +imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } +imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } + [[test]] name = "imap_features" path = "tests/imap_features.rs" From d2c3b641fea6106d0fa2a7940abbc026e003f707 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 1 Jan 2024 09:34:13 +0100 Subject: [PATCH 10/18] WIP rewrite --- src/imap/command/anonymous.rs | 19 +++++++++---------- src/imap/command/authenticated.rs | 12 ++++++------ src/imap/command/examined.rs | 16 ++++++++-------- src/imap/command/selected.rs | 10 +++++----- src/imap/mailbox_view.rs | 20 ++++++++++---------- src/imap/mod.rs | 27 ++++++++++++++------------- src/imap/session.rs | 10 ++++++---- 7 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 6ba19cf..9f4563f 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,8 +1,7 @@ use anyhow::{Error, Result}; -use boitalettres::proto::{res::body::Data as Body, Request, Response}; -use imap_codec::types::command::CommandBody; -use imap_codec::types::core::AString; -use imap_codec::types::response::{Capability, Data, Status}; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::AString; +use imap_codec::imap_types::response::{Capability, Data, Status, CommandContinuationRequest}; use crate::imap::flow; use crate::login::ArcLoginProvider; @@ -11,12 +10,12 @@ use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub login_provider: Option<&'a ArcLoginProvider>, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Status, flow::Transition)> { + match &ctx.req.body { CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)), CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, @@ -31,7 +30,7 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Tran //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Response, flow::Transition)> { + async fn capability(self) -> Result<(Status, flow::Transition)> { let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities)); Ok((res, flow::Transition::None)) @@ -41,7 +40,7 @@ impl<'a> AnonymousContext<'a> { self, username: &AString, password: &AString, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Status, flow::Transition)> { let (u, p) = ( String::try_from(username.clone())?, String::try_from(password.clone())?, @@ -81,7 +80,7 @@ impl<'a> AnonymousContext<'a> { // C: 10 logout // S: * BYE Logging out // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Response, flow::Transition)> { + async fn logout(self) -> Result<(Status, flow::Transition)> { // @FIXME we should implement From> and From> in // boitalettres/src/proto/res/body.rs Ok(( diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 2deb723..fc58425 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -4,12 +4,12 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use boitalettres::proto::res::body::Data as Body; use boitalettres::proto::{Request, Response}; -use imap_codec::types::command::{CommandBody, StatusAttribute}; -use imap_codec::types::core::NonZeroBytes; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::flag::{Flag, FlagNameAttribute}; -use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::types::response::{Code, Data, StatusAttributeValue}; +use imap_codec::imap_types::command::{CommandBody, StatusAttribute}; +use imap_codec::imap_types::core::NonZeroBytes; +use imap_codec::imap_types::datetime::MyDateTime; +use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; +use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; +use imap_codec::imap_types::response::{Code, Data, StatusAttributeValue}; use crate::imap::command::anonymous; use crate::imap::flow; diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 1740b39..8037d1d 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -3,14 +3,14 @@ use std::sync::Arc; use anyhow::Result; use boitalettres::proto::Request; use boitalettres::proto::Response; -use imap_codec::types::command::{CommandBody, SearchKey}; -use imap_codec::types::core::{Charset, NonZeroBytes}; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; -use imap_codec::types::flag::Flag; -use imap_codec::types::mailbox::Mailbox as MailboxCodec; -use imap_codec::types::response::Code; -use imap_codec::types::sequence::SequenceSet; +use imap_codec::imap_types::command::{CommandBody, SearchKey}; +use imap_codec::imap_types::core::{Charset, NonZeroBytes}; +use imap_codec::imap_types::datetime::MyDateTime; +use imap_codec::imap_types::fetch_attributes::MacroOrFetchAttributes; +use imap_codec::imap_types::flag::Flag; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; +use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::sequence::SequenceSet; use crate::imap::command::authenticated; use crate::imap::flow; diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 90a00ee..6bf068c 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use anyhow::Result; use boitalettres::proto::Request; use boitalettres::proto::Response; -use imap_codec::types::command::CommandBody; -use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::types::mailbox::Mailbox as MailboxCodec; -use imap_codec::types::response::Code; -use imap_codec::types::sequence::SequenceSet; +use imap_codec::imap_types::command::CommandBody; +use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; +use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::sequence::SequenceSet; use crate::imap::command::examined; use crate::imap::flow; diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index f896448..d9baf47 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -9,17 +9,17 @@ use chrono::{Offset, TimeZone, Utc}; use futures::stream::{FuturesOrdered, StreamExt}; -use imap_codec::types::address::Address; -use imap_codec::types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; -use imap_codec::types::core::{AString, Atom, IString, NString}; -use imap_codec::types::datetime::MyDateTime; -use imap_codec::types::envelope::Envelope; -use imap_codec::types::fetch_attributes::{ +use imap_codec::imap_types::address::Address; +use imap_codec::imap_types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; +use imap_codec::imap_types::core::{AString, Atom, IString, NString}; +use imap_codec::imap_types::datetime::MyDateTime; +use imap_codec::imap_types::envelope::Envelope; +use imap_codec::imap_types::fetch_attributes::{ FetchAttribute, MacroOrFetchAttributes, Section as FetchSection, }; -use imap_codec::types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::types::response::{Code, Data, MessageAttribute, Status}; -use imap_codec::types::sequence::{self, SequenceSet}; +use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; +use imap_codec::imap_types::response::{Code, Data, MessageAttribute, Status}; +use imap_codec::imap_types::sequence::{self, SequenceSet}; use eml_codec::{ header, imf, mime, @@ -1246,7 +1246,7 @@ mod tests { use crate::cryptoblob; use crate::mail::unique_ident; use imap_codec::codec::Encode; - use imap_codec::types::fetch_attributes::Section; + use imap_codec::imap_types::fetch_attributes::Section; use std::fs; #[test] diff --git a/src/imap/mod.rs b/src/imap/mod.rs index f85bcc6..73cd943 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -6,45 +6,45 @@ mod session; use std::task::{Context, Poll}; use anyhow::Result; -use boitalettres::errors::Error as BalError; -use boitalettres::proto::{Request, Response}; -use boitalettres::server::accept::addr::AddrIncoming; -use boitalettres::server::accept::addr::AddrStream; -use boitalettres::server::Server as ImapServer; +//use boitalettres::errors::Error as BalError; +//use boitalettres::proto::{Request, Response}; +//use boitalettres::server::accept::addr::AddrIncoming; +//use boitalettres::server::accept::addr::AddrStream; +//use boitalettres::server::Server as ImapServer; use futures::future::BoxFuture; use futures::future::FutureExt; use tokio::sync::watch; -use tower::Service; use crate::config::ImapConfig; use crate::login::ArcLoginProvider; /// Server is a thin wrapper to register our Services in BàL -pub struct Server(ImapServer); +pub struct Server{} pub async fn new(config: ImapConfig, login: ArcLoginProvider) -> Result { - //@FIXME add a configuration parameter - let incoming = AddrIncoming::new(config.bind_addr).await?; + unimplemented!(); + /* let incoming = AddrIncoming::new(config.bind_addr).await?; tracing::info!("IMAP activated, will listen on {:#}", incoming.local_addr); let imap = ImapServer::new(incoming).serve(Instance::new(login.clone())); - Ok(Server(imap)) + Ok(Server(imap))*/ } impl Server { pub async fn run(self, mut must_exit: watch::Receiver) -> Result<()> { tracing::info!("IMAP started!"); - tokio::select! { + unimplemented!(); + /*tokio::select! { s = self.0 => s?, _ = must_exit.changed() => tracing::info!("Stopped IMAP server"), } - Ok(()) + Ok(())*/ } } //--- - +/* /// Instance is the main Tokio Tower service that we register in BàL. /// It receives new connection demands and spawn a dedicated service. struct Instance { @@ -103,3 +103,4 @@ impl Service for Connection { self.session.process(req) } } +*/ diff --git a/src/imap/session.rs b/src/imap/session.rs index 15141d3..e2af18b 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,6 +1,6 @@ use anyhow::Error; -use boitalettres::errors::Error as BalError; -use boitalettres::proto::{Request, Response}; +//use boitalettres::errors::Error as BalError; +//use boitalettres::proto::{Request, Response}; use futures::future::BoxFuture; use futures::future::FutureExt; @@ -11,6 +11,7 @@ use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::login::ArcLoginProvider; +/* /* This constant configures backpressure in the system, * or more specifically, how many pipelined messages are allowed * before refusing them @@ -69,9 +70,9 @@ impl Manager { .boxed() } } - +*/ //----- - +/* pub struct Instance { rx: mpsc::Receiver, @@ -178,3 +179,4 @@ impl Instance { tracing::debug!("exiting runner"); } } +*/ From e2d77defc8496c2795860c6901d752e2c8d1c4ac Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 1 Jan 2024 17:54:48 +0100 Subject: [PATCH 11/18] fixed anonymous + authenticated imap logic --- src/imap/command/anonymous.rs | 85 +++---- src/imap/command/authenticated.rs | 361 ++++++++++++++++++++---------- src/imap/mod.rs | 3 +- src/imap/response.rs | 97 ++++++++ tests/imap_features.rs | 2 +- 5 files changed, 390 insertions(+), 158 deletions(-) create mode 100644 src/imap/response.rs diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 9f4563f..9bbb3b7 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,9 +1,11 @@ -use anyhow::{Error, Result}; +use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::AString; -use imap_codec::imap_types::response::{Capability, Data, Status, CommandContinuationRequest}; +use imap_codec::imap_types::core::{AString, NonEmptyVec}; +use imap_codec::imap_types::response::{Capability, Data}; +use imap_codec::imap_types::secret::Secret; use crate::imap::flow; +use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -11,18 +13,30 @@ use crate::mail::user::User; pub struct AnonymousContext<'a> { pub req: &'a Command<'static>, - pub login_provider: Option<&'a ArcLoginProvider>, + pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Status, flow::Transition)> { +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { match &ctx.req.body { - CommandBody::Noop => Ok((Response::ok("Noop completed.")?, flow::Transition::None)), + CommandBody::Noop => Ok(( + Response::ok() + .to_req(ctx.req) + .message("Noop completed.") + .build()?, + flow::Transition::None, + )), CommandBody::Capability => ctx.capability().await, CommandBody::Logout => ctx.logout().await, CommandBody::Login { username, password } => ctx.login(username, password).await, cmd => { - tracing::warn!("Unknown command {:?}", cmd); - Ok((Response::no("Command unavailable")?, flow::Transition::None)) + tracing::warn!("Unknown command for the anonymous state {:?}", cmd); + Ok(( + Response::bad() + .to_req(ctx.req) + .message("Command unavailable") + .build()?, + flow::Transition::None, + )) } } } @@ -30,49 +44,50 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Status, flow::Transi //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Status, flow::Transition)> { - let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; - let res = Response::ok("Server capabilities")?.with_body(Data::Capability(capabilities)); + async fn capability(self) -> Result<(Response, flow::Transition)> { + let capabilities: NonEmptyVec = + (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let res = Response::ok() + .to_req(self.req) + .message("Server capabilities") + .data(Data::Capability(capabilities)) + .build()?; Ok((res, flow::Transition::None)) } async fn login( self, - username: &AString, - password: &AString, - ) -> Result<(Status, flow::Transition)> { + username: &AString<'a>, + password: &Secret>, + ) -> Result<(Response, flow::Transition)> { let (u, p) = ( - String::try_from(username.clone())?, - String::try_from(password.clone())?, + std::str::from_utf8(username.as_ref())?, + std::str::from_utf8(password.declassify().as_ref())?, ); tracing::info!(user = %u, "command.login"); - let login_provider = match &self.login_provider { - Some(lp) => lp, - None => { - return Ok(( - Response::no("Login command not available (already logged in)")?, - flow::Transition::None, - )) - } - }; - - let creds = match login_provider.login(&u, &p).await { + let creds = match self.login_provider.login(&u, &p).await { Err(e) => { tracing::debug!(error=%e, "authentication failed"); return Ok(( - Response::no("Authentication failed")?, + Response::no() + .to_req(self.req) + .message("Authentication failed") + .build()?, flow::Transition::None, )); } Ok(c) => c, }; - let user = User::new(u.clone(), creds).await?; + let user = User::new(u.to_string(), creds).await?; tracing::info!(username=%u, "connected"); Ok(( - Response::ok("Completed")?, + Response::ok() + .to_req(self.req) + .message("Completed") + .build()?, flow::Transition::Authenticate(user), )) } @@ -80,15 +95,9 @@ impl<'a> AnonymousContext<'a> { // C: 10 logout // S: * BYE Logging out // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Status, flow::Transition)> { + async fn logout(self) -> Result<(Response, flow::Transition)> { // @FIXME we should implement From> and From> in // boitalettres/src/proto/res/body.rs - Ok(( - Response::ok("Logout completed")?.with_body(vec![Body::Status( - Status::bye(None, "Logging out") - .map_err(|e| Error::msg(e).context("Unable to generate IMAP status"))?, - )]), - flow::Transition::Logout, - )) + Ok((Response::bye()?, flow::Transition::Logout)) } } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index fc58425..073b005 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -2,37 +2,35 @@ use std::collections::BTreeMap; use std::sync::Arc; use anyhow::{anyhow, bail, Result}; -use boitalettres::proto::res::body::Data as Body; -use boitalettres::proto::{Request, Response}; -use imap_codec::imap_types::command::{CommandBody, StatusAttribute}; -use imap_codec::imap_types::core::NonZeroBytes; -use imap_codec::imap_types::datetime::MyDateTime; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::{Atom, Literal, QuotedChar}; +use imap_codec::imap_types::datetime::DateTime; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::imap_types::response::{Code, Data, StatusAttributeValue}; +use imap_codec::imap_types::response::{Code, CodeOther, Data}; +use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; -use crate::imap::command::anonymous; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; -use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER}; +use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; +static MAILBOX_HIERARCHY_DELIMITER: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); + pub struct AuthenticatedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'static>, pub user: &'a Arc, } pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { + match &ctx.req.body { CommandBody::Create { mailbox } => ctx.create(mailbox).await, CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, - CommandBody::Rename { - mailbox, - new_mailbox, - } => ctx.rename(mailbox, new_mailbox).await, + CommandBody::Rename { from, to } => ctx.rename(from, to).await, CommandBody::Lsub { reference, mailbox_wildcard, @@ -43,8 +41,8 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: } => ctx.list(reference, mailbox_wildcard, false).await, CommandBody::Status { mailbox, - attributes, - } => ctx.status(mailbox, attributes).await, + item_names, + } => ctx.status(mailbox, item_names).await, CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, CommandBody::Select { mailbox } => ctx.select(mailbox).await, @@ -55,90 +53,161 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: date, message, } => ctx.append(mailbox, flags, date, message).await, - _ => { - let ctx = anonymous::AnonymousContext { - req: ctx.req, - login_provider: None, - }; - anonymous::dispatch(ctx).await + cmd => { + tracing::warn!("Unknown command for the authenticated state {:?}", cmd); + Ok(( + Response::bad() + .to_req(ctx.req) + .message("Command unavailable") + .build()?, + flow::Transition::None, + )) } } } // --- PRIVATE --- -impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - - if name == INBOX { - return Ok(( - Response::bad("Cannot create INBOX")?, - flow::Transition::None, - )); +/// Convert an IMAP mailbox name/identifier representation +/// to an utf-8 string that is used internally in Aerogramme +struct MailboxName<'a>(&'a MailboxCodec<'a>); +impl<'a> TryInto<&'a str> for MailboxName<'a> { + type Error = std::str::Utf8Error; + fn try_into(self) -> Result<&'a str, Self::Error> { + match self.0 { + MailboxCodec::Inbox => Ok(INBOX), + MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), } + } +} + +impl<'a> AuthenticatedContext<'a> { + async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name = match mailbox { + MailboxCodec::Inbox => { + return Ok(( + Response::bad() + .to_req(self.req) + .message("Cannot create INBOX") + .build()?, + flow::Transition::None, + )); + } + MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?, + }; 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)), + Ok(()) => Ok(( + Response::ok() + .to_req(self.req) + .message("CREATE complete") + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(&e.to_string()) + .build()?, + flow::Transition::None, + )), } } - async fn delete(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; 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)), + Ok(()) => Ok(( + Response::ok() + .to_req(self.req) + .message("DELETE complete") + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, + flow::Transition::None, + )), } } async fn rename( self, - mailbox: &MailboxCodec, - new_mailbox: &MailboxCodec, + from: &MailboxCodec<'a>, + to: &MailboxCodec<'a>, ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - let new_name = String::try_from(new_mailbox.clone())?; + let name: &str = MailboxName(from).try_into()?; + let new_name: &str = MailboxName(to).try_into()?; 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)), + Ok(()) => Ok(( + Response::ok() + .to_req(self.req) + .message("RENAME complete") + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, + flow::Transition::None, + )), } } async fn list( self, - reference: &MailboxCodec, - mailbox_wildcard: &ListMailbox, + reference: &MailboxCodec<'a>, + mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, ) -> Result<(Response, flow::Transition)> { - let reference = String::try_from(reference.clone())?; + let reference: &str = MailboxName(reference).try_into()?; if !reference.is_empty() { return Ok(( - Response::bad("References not supported")?, + Response::bad() + .to_req(self.req) + .message("References not supported") + .build()?, flow::Transition::None, )); } - let wildcard = String::try_from(mailbox_wildcard.clone())?; + // @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())?, + ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?, + }; if wildcard.is_empty() { if is_lsub { return Ok(( - Response::ok("LSUB complete")?.with_body(vec![Data::Lsub { - items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), - mailbox: "".try_into().unwrap(), - }]), + Response::ok() + .to_req(self.req) + .message("LSUB complete") + .data(Data::Lsub { + items: vec![], + delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + mailbox: "".try_into().unwrap(), + }) + .build()?, flow::Transition::None, )); } else { return Ok(( - Response::ok("LIST complete")?.with_body(vec![Data::List { - items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), - mailbox: "".try_into().unwrap(), - }]), + Response::ok() + .to_req(self.req) + .message("LIST complete") + .data(Data::List { + items: vec![], + delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + mailbox: "".try_into().unwrap(), + }) + .build()?, flow::Transition::None, )); } @@ -147,7 +216,7 @@ impl<'a> AuthenticatedContext<'a> { 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) { + for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { if i > 0 { let smb = &mb[..i]; vmailboxes.entry(smb).or_insert(false); @@ -163,9 +232,9 @@ impl<'a> AuthenticatedContext<'a> { .to_string() .try_into() .map_err(|_| anyhow!("invalid mailbox name"))?; - let mut items = vec![FlagNameAttribute::Extension( - "Subscribed".try_into().unwrap(), - )]; + let mut items = vec![FlagNameAttribute::try_from(Atom::unvalidated( + "Subscribed", + ))?]; if !*is_real { items.push(FlagNameAttribute::Noselect); } @@ -190,21 +259,31 @@ impl<'a> AuthenticatedContext<'a> { } else { "LIST completed" }; - Ok((Response::ok(msg)?.with_body(ret), flow::Transition::None)) + Ok(( + Response::ok() + .to_req(self.req) + .message(msg) + .set_data(ret) + .build()?, + flow::Transition::None, + )) } async fn status( self, - mailbox: &MailboxCodec, - attributes: &[StatusAttribute], + mailbox: &MailboxCodec<'a>, + attributes: &[StatusDataItemName], ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; - let mb_opt = self.user.open_mailbox(&name).await?; + let name: &str = MailboxName(mailbox).try_into()?; + let mb_opt = self.user.open_mailbox(name).await?; let mb = match mb_opt { Some(mb) => mb, None => { return Ok(( - Response::no("Mailbox does not exist")?, + Response::no() + .to_req(self.req) + .message("Mailbox does not exist") + .build()?, flow::Transition::None, )) } @@ -215,54 +294,79 @@ impl<'a> AuthenticatedContext<'a> { let mut ret_attrs = vec![]; for attr in attributes.iter() { ret_attrs.push(match attr { - StatusAttribute::Messages => StatusAttributeValue::Messages(view.exists()?), - StatusAttribute::Unseen => StatusAttributeValue::Unseen(view.unseen_count() as u32), - StatusAttribute::Recent => StatusAttributeValue::Recent(view.recent()?), - StatusAttribute::UidNext => StatusAttributeValue::UidNext(view.uidnext()), - StatusAttribute::UidValidity => { - StatusAttributeValue::UidValidity(view.uidvalidity()) + StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?), + StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32), + StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?), + StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()), + StatusDataItemName::UidValidity => { + StatusDataItem::UidValidity(view.uidvalidity()) } + StatusDataItemName::Deleted => { + bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE"); + }, + StatusDataItemName::DeletedStorage => { + bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); + }, }); } - let data = vec![Body::Data(Data::Status { + let data = Data::Status { mailbox: mailbox.clone(), - attributes: ret_attrs, - })]; + items: ret_attrs.into(), + }; Ok(( - Response::ok("STATUS completed")?.with_body(data), + Response::ok() + .to_req(self.req) + .message("STATUS completed") + .data(data) + .build()?, flow::Transition::None, )) } - async fn subscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn subscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { - Ok((Response::ok("SUBSCRIBE complete")?, flow::Transition::None)) + Ok(( + Response::ok() + .to_req(self.req) + .message("SUBSCRIBE complete") + .build()?, + flow::Transition::None, + )) } else { Ok(( - Response::bad(&format!("Mailbox {} does not exist", name))?, + Response::bad() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .build()?, flow::Transition::None, )) } } - async fn unsubscribe(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn unsubscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { Ok(( - Response::bad(&format!( - "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", - name - ))?, + Response::bad() + .to_req(self.req) + .message(format!( + "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", + name + )) + .build()?, flow::Transition::None, )) } else { Ok(( - Response::bad(&format!("Mailbox {} does not exist", name))?, + Response::no() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .build()?, flow::Transition::None, )) } @@ -301,15 +405,18 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; let mb = match mb_opt { Some(mb) => mb, None => { return Ok(( - Response::no("Mailbox does not exist")?, + Response::no() + .to_req(self.req) + .message("Mailbox does not exist") + .build()?, flow::Transition::None, )) } @@ -319,22 +426,27 @@ impl<'a> AuthenticatedContext<'a> { let (mb, data) = MailboxView::new(mb).await?; Ok(( - Response::ok("Select completed")? - .with_extra_code(Code::ReadWrite) - .with_body(data), + Response::ok() + .message("Select completed") + .code(Code::ReadWrite) + .data(data) + .build()?, flow::Transition::Select(mb), )) } - async fn examine(self, mailbox: &MailboxCodec) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; let mb = match mb_opt { Some(mb) => mb, None => { return Ok(( - Response::no("Mailbox does not exist")?, + Response::no() + .to_req(self.req) + .message("Mailbox does not exist") + .build()?, flow::Transition::None, )) } @@ -344,40 +456,53 @@ impl<'a> AuthenticatedContext<'a> { let (mb, data) = MailboxView::new(mb).await?; Ok(( - Response::ok("Examine completed")? - .with_extra_code(Code::ReadOnly) - .with_body(data), + Response::ok() + .to_req(self.req) + .message("Examine completed") + .code(Code::ReadOnly) + .data(data) + .build()?, flow::Transition::Examine(mb), )) } async fn append( self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option, - message: &NonZeroBytes, + mailbox: &MailboxCodec<'a>, + flags: &[Flag<'a>], + date: &Option, + message: &Literal<'a>, ) -> Result<(Response, flow::Transition)> { + let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { Ok((_mb, uidvalidity, uid)) => Ok(( - Response::ok("APPEND completed")?.with_extra_code(Code::Other( - "APPENDUID".try_into().unwrap(), - Some(format!("{} {}", uidvalidity, uid)), - )), + Response::ok() + .tag(append_tag) + .message("APPEND completed") + .code(Code::Other(CodeOther::unvalidated( + format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), + ))) + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .tag(append_tag) + .message(e.to_string()) + .build()?, flow::Transition::None, )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), } } pub(crate) async fn append_internal( self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option, - message: &NonZeroBytes, + mailbox: &MailboxCodec<'a>, + flags: &[Flag<'a>], + date: &Option, + message: &Literal<'a>, ) -> Result<(Arc, ImapUidvalidity, ImapUidvalidity)> { - let name = String::try_from(mailbox.clone())?; + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; let mb = match mb_opt { @@ -389,8 +514,8 @@ impl<'a> AuthenticatedContext<'a> { bail!("Cannot set date when appending message"); } - let msg = IMF::try_from(message.as_slice()) - .map_err(|_| anyhow!("Could not parse e-mail message"))?; + let msg = + IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?; let flags = flags.iter().map(|x| x.to_string()).collect::>(); // TODO: filter allowed flags? ping @Quentin @@ -422,7 +547,7 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool { && j > 0 && matches[i - 1][j] && (wildcard[j - 1] == '*' - || (wildcard[j - 1] == '%' && name[i - 1] != MAILBOX_HIERARCHY_DELIMITER))); + || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); } } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 73cd943..589231b 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,6 +1,7 @@ mod command; mod flow; mod mailbox_view; +mod response; mod session; use std::task::{Context, Poll}; @@ -19,7 +20,7 @@ use crate::config::ImapConfig; use crate::login::ArcLoginProvider; /// Server is a thin wrapper to register our Services in BàL -pub struct Server{} +pub struct Server {} pub async fn new(config: ImapConfig, login: ArcLoginProvider) -> Result { unimplemented!(); diff --git a/src/imap/response.rs b/src/imap/response.rs new file mode 100644 index 0000000..22e91f3 --- /dev/null +++ b/src/imap/response.rs @@ -0,0 +1,97 @@ +use anyhow::Result; +use imap_codec::imap_types::command::Command; +use imap_codec::imap_types::core::Tag; +use imap_codec::imap_types::response::{Code, Data, Status, StatusKind}; + +pub struct ResponseBuilder { + status: StatusKind, + tag: Option>, + code: Option>, + text: String, + data: Vec>, +} + +impl<'a> Default for ResponseBuilder { + fn default() -> ResponseBuilder { + ResponseBuilder { + status: StatusKind::Bad, + tag: None, + code: None, + text: "".to_string(), + data: vec![], + } + } +} + +impl ResponseBuilder { + pub fn to_req(mut self, cmd: &Command) -> Self { + self.tag = Some(cmd.tag); + self + } + pub fn tag(mut self, tag: Tag) -> Self { + self.tag = Some(tag); + self + } + + pub fn message(mut self, txt: impl Into) -> Self { + self.text = txt.into(); + self + } + + pub fn code(mut self, code: Code) -> Self { + self.code = Some(code); + self + } + + pub fn data(mut self, data: Data) -> Self { + self.data.push(data); + self + } + + pub fn set_data(mut self, data: Vec) -> Self { + self.data = data; + self + } + + pub fn build(self) -> Result { + Ok(Response { + status: Status::new(self.tag, self.status, self.code, self.text)?, + data: self.data, + }) + } +} + +pub struct Response { + data: Vec>, + status: Status<'static>, +} + +impl Response { + pub fn ok() -> ResponseBuilder { + ResponseBuilder { + status: StatusKind::Ok, + ..ResponseBuilder::default() + } + } + + pub fn no() -> ResponseBuilder { + ResponseBuilder { + status: StatusKind::No, + ..ResponseBuilder::default() + } + } + + pub fn bad() -> ResponseBuilder { + ResponseBuilder { + status: StatusKind::Bad, + ..ResponseBuilder::default() + } + } + + pub fn bye() -> Result { + Ok(Response { + status: Status::bye(None, "bye")?, + data: vec![], + }) + } +} diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 844b176..3479292 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -187,7 +187,7 @@ fn select_inbox(imap: &mut TcpStream) -> Result<()> { } fn status_mailbox(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"25 STATUS archive (UIDNEXT MESSAGES)\r\n"[..])?; + imap.write(&b"25 STATUS archive (UIDNEXT MESSAGES)\r\n"[..])?; let mut buffer: [u8; 6000] = [0; 6000]; let _read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?; From 07eea38765aecbd53e51be199094eba2871dc7ad Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 1 Jan 2024 19:25:28 +0100 Subject: [PATCH 12/18] ported commands --- src/imap/command/anonymous.rs | 40 +++------ src/imap/command/anystate.rs | 49 ++++++++++ src/imap/command/authenticated.rs | 36 +++----- src/imap/command/examined.rs | 132 +++++++++++++-------------- src/imap/command/mod.rs | 17 ++++ src/imap/command/selected.rs | 143 ++++++++++++++++++++++++------ 6 files changed, 268 insertions(+), 149 deletions(-) create mode 100644 src/imap/command/anystate.rs diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 9bbb3b7..42e2a87 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -4,6 +4,7 @@ use imap_codec::imap_types::core::{AString, NonEmptyVec}; use imap_codec::imap_types::response::{Capability, Data}; use imap_codec::imap_types::secret::Secret; +use crate::imap::command::anystate; use crate::imap::flow; use crate::imap::response::Response; use crate::login::ArcLoginProvider; @@ -18,26 +19,20 @@ pub struct AnonymousContext<'a> { pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { match &ctx.req.body { - CommandBody::Noop => Ok(( - Response::ok() - .to_req(ctx.req) - .message("Noop completed.") - .build()?, - flow::Transition::None, - )), - CommandBody::Capability => ctx.capability().await, - CommandBody::Logout => ctx.logout().await, + // Any State + CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to anonymous context (3 commands) CommandBody::Login { username, password } => ctx.login(username, password).await, - cmd => { - tracing::warn!("Unknown command for the anonymous state {:?}", cmd); - Ok(( - Response::bad() - .to_req(ctx.req) - .message("Command unavailable") - .build()?, - flow::Transition::None, - )) + CommandBody::Authenticate { .. } => { + anystate::not_implemented(ctx.req.tag.clone(), "authenticate") } + //StartTLS is not implemented for now, we will probably go full TLS. + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), } } @@ -91,13 +86,4 @@ impl<'a> AnonymousContext<'a> { flow::Transition::Authenticate(user), )) } - - // C: 10 logout - // S: * BYE Logging out - // S: 10 OK Logout completed. - async fn logout(self) -> Result<(Response, flow::Transition)> { - // @FIXME we should implement From> and From> in - // boitalettres/src/proto/res/body.rs - Ok((Response::bye()?, flow::Transition::Logout)) - } } diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs new file mode 100644 index 0000000..2d10ad8 --- /dev/null +++ b/src/imap/command/anystate.rs @@ -0,0 +1,49 @@ +use anyhow::Result; +use imap_codec::imap_types::core::{NonEmptyVec, Tag}; +use imap_codec::imap_types::response::{Capability, Data}; + +use crate::imap::flow; +use crate::imap::response::Response; + +pub(crate) fn capability(tag: Tag) -> Result<(Response, flow::Transition)> { + let capabilities: NonEmptyVec = + (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let res = Response::ok() + .tag(tag) + .message("Server capabilities") + .data(Data::Capability(capabilities)) + .build()?; + + Ok((res, flow::Transition::None)) +} + +pub(crate) fn noop_nothing(tag: Tag) -> Result<(Response, flow::Transition)> { + Ok(( + Response::ok().tag(tag).message("Noop completed.").build()?, + flow::Transition::None, + )) +} + +pub(crate) fn logout() -> Result<(Response, flow::Transition)> { + Ok((Response::bye()?, flow::Transition::Logout)) +} + +pub(crate) fn not_implemented(tag: Tag, what: &str) -> Result<(Response, flow::Transition)> { + Ok(( + Response::bad() + .tag(tag) + .message(format!("Command not implemented {}", what)) + .build()?, + flow::Transition::None, + )) +} + +pub(crate) fn wrong_state(tag: Tag) -> Result<(Response, flow::Transition)> { + Ok(( + Response::bad() + .tag(tag) + .message("Command not authorized in this state") + .build()?, + flow::Transition::None, + )) +} diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 073b005..ca4ad03 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -10,13 +10,14 @@ use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; +use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; -use crate::mail::user::{User, INBOX, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; +use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; static MAILBOX_HIERARCHY_DELIMITER: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); @@ -28,6 +29,12 @@ pub struct AuthenticatedContext<'a> { pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { match &ctx.req.body { + // Any state + CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to this state (11 commands) CommandBody::Create { mailbox } => ctx.create(mailbox).await, CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, CommandBody::Rename { from, to } => ctx.rename(from, to).await, @@ -53,34 +60,13 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: date, message, } => ctx.append(mailbox, flags, date, message).await, - cmd => { - tracing::warn!("Unknown command for the authenticated state {:?}", cmd); - Ok(( - Response::bad() - .to_req(ctx.req) - .message("Command unavailable") - .build()?, - flow::Transition::None, - )) - } + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), } } // --- PRIVATE --- - -/// Convert an IMAP mailbox name/identifier representation -/// to an utf-8 string that is used internally in Aerogramme -struct MailboxName<'a>(&'a MailboxCodec<'a>); -impl<'a> TryInto<&'a str> for MailboxName<'a> { - type Error = std::str::Utf8Error; - fn try_into(self) -> Result<&'a str, Self::Error> { - match self.0 { - MailboxCodec::Inbox => Ok(INBOX), - MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), - } - } -} - impl<'a> AuthenticatedContext<'a> { async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { let name = match mailbox { diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 8037d1d..cab3fdd 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -1,89 +1,111 @@ use std::sync::Arc; use anyhow::Result; -use boitalettres::proto::Request; -use boitalettres::proto::Response; -use imap_codec::imap_types::command::{CommandBody, SearchKey}; -use imap_codec::imap_types::core::{Charset, NonZeroBytes}; -use imap_codec::imap_types::datetime::MyDateTime; -use imap_codec::imap_types::fetch_attributes::MacroOrFetchAttributes; -use imap_codec::imap_types::flag::Flag; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; -use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::Charset; +use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; +use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::command::authenticated; +use crate::imap::command::anystate; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::user::User; pub struct ExaminedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'a>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - // CLOSE in examined state is not the same as in selected state - // (in selected state it also does an EXPUNGE, here it doesn't) + match &ctx.req.body { + // Any State + // noop is specific to this state + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to the EXAMINE state (specialization of the SELECTED state) + // ~3 commands -> close, fetch, search + NOOP CommandBody::Close => ctx.close().await, CommandBody::Fetch { sequence_set, - attributes, + macro_or_item_names, uid, - } => ctx.fetch(sequence_set, attributes, uid).await, + } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, CommandBody::Search { charset, criteria, uid, } => ctx.search(charset, criteria, uid).await, - CommandBody::Noop => ctx.noop().await, - CommandBody::Append { - mailbox, - flags, - date, - message, - } => ctx.append(mailbox, flags, date, message).await, - _ => { - let ctx = authenticated::AuthenticatedContext { - req: ctx.req, - user: ctx.user, - }; - authenticated::dispatch(ctx).await - } + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok(( + Response::bad() + .to_req(ctx.req) + .message("Forbidden command: can't write in read-only mode (EXAMINE)") + .build()?, + flow::Transition::None, + )), + + // The command does not belong to this state + _ => anystate::wrong_state(ctx.req.tag.clone()), } } // --- PRIVATE --- impl<'a> ExaminedContext<'a> { + /// CLOSE in examined state is not the same as in selected state + /// (in selected state it also does an EXPUNGE, here it doesn't) async fn close(self) -> Result<(Response, flow::Transition)> { - Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) + Ok(( + Response::ok() + .to_req(self.req) + .message("CLOSE completed") + .build()?, + flow::Transition::Unselect, + )) } pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrFetchAttributes, + attributes: &MacroOrMessageDataItemNames<'a>, uid: &bool, ) -> Result<(Response, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok("FETCH completed")?.with_body(resp), + Response::ok() + .to_req(self.req) + .message("FETCH completed") + .set_data(resp) + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, flow::Transition::None, )), - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), } } pub async fn search( self, - _charset: &Option, - _criteria: &SearchKey, + _charset: &Option>, + _criteria: &SearchKey<'a>, _uid: &bool, ) -> Result<(Response, flow::Transition)> { - Ok((Response::bad("Not implemented")?, flow::Transition::None)) + Ok(( + Response::bad() + .to_req(self.req) + .message("Not implemented") + .build()?, + flow::Transition::None, + )) } pub async fn noop(self) -> Result<(Response, flow::Transition)> { @@ -91,38 +113,12 @@ impl<'a> ExaminedContext<'a> { let updates = self.mailbox.update().await?; Ok(( - Response::ok("NOOP completed.")?.with_body(updates), + Response::ok() + .to_req(self.req) + .message("NOOP completed.") + .set_data(updates) + .build()?, flow::Transition::None, )) } - - async fn append( - self, - mailbox: &MailboxCodec, - flags: &[Flag], - date: &Option, - message: &NonZeroBytes, - ) -> Result<(Response, flow::Transition)> { - let ctx2 = authenticated::AuthenticatedContext { - req: self.req, - user: self.user, - }; - - match ctx2.append_internal(mailbox, flags, date, message).await { - Ok((mb, uidvalidity, uid)) => { - let resp = Response::ok("APPEND completed")?.with_extra_code(Code::Other( - "APPENDUID".try_into().unwrap(), - Some(format!("{} {}", uidvalidity, uid)), - )); - - if Arc::ptr_eq(&mb, &self.mailbox.mailbox) { - let data = self.mailbox.update().await?; - Ok((resp.with_body(data), flow::Transition::None)) - } else { - Ok((resp, flow::Transition::None)) - } - } - Err(e) => Ok((Response::no(&e.to_string())?, flow::Transition::None)), - } - } } diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs index 0b7e576..dc95746 100644 --- a/src/imap/command/mod.rs +++ b/src/imap/command/mod.rs @@ -1,4 +1,21 @@ pub mod anonymous; +pub mod anystate; pub mod authenticated; pub mod examined; pub mod selected; + +use crate::mail::user::INBOX; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; + +/// Convert an IMAP mailbox name/identifier representation +/// to an utf-8 string that is used internally in Aerogramme +struct MailboxName<'a>(&'a MailboxCodec<'a>); +impl<'a> TryInto<&'a str> for MailboxName<'a> { + type Error = std::str::Utf8Error; + fn try_into(self) -> Result<&'a str, Self::Error> { + match self.0 { + MailboxCodec::Inbox => Ok(INBOX), + MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), + } + } +} diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 6bf068c..148901d 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -1,31 +1,48 @@ use std::sync::Arc; use anyhow::Result; -use boitalettres::proto::Request; -use boitalettres::proto::Response; -use imap_codec::imap_types::command::CommandBody; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::Charset; +use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; -use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::response::{Code, CodeOther}; +use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::command::examined; +use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; +use crate::imap::response::Response; use crate::mail::user::User; pub struct SelectedContext<'a> { - pub req: &'a Request, + pub req: &'a Command<'a>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> { - match &ctx.req.command.body { - // Only write commands here, read commands are handled in - // `examined.rs` + match &ctx.req.body { + // Any State + // noop is specific to this state + CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + + // Specific to this state (7 commands + NOOP) CommandBody::Close => ctx.close().await, + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Fetch { + sequence_set, + macro_or_item_names, + uid, + } => ctx.fetch(sequence_set, macro_or_item_names, uid).await, + CommandBody::Search { + charset, + criteria, + uid, + } => ctx.search(charset, criteria, uid).await, CommandBody::Expunge => ctx.expunge().await, CommandBody::Store { sequence_set, @@ -39,14 +56,9 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans mailbox, uid, } => ctx.copy(sequence_set, mailbox, uid).await, - _ => { - let ctx = examined::ExaminedContext { - req: ctx.req, - user: ctx.user, - mailbox: ctx.mailbox, - }; - examined::dispatch(ctx).await - } + + // The command does not belong to this state + _ => anystate::wrong_state(ctx.req.tag.clone()), } } @@ -56,15 +68,78 @@ impl<'a> SelectedContext<'a> { async fn close(self) -> Result<(Response, flow::Transition)> { // We expunge messages, // but we don't send the untagged EXPUNGE responses + let tag = self.req.tag.clone(); self.expunge().await?; - Ok((Response::ok("CLOSE completed")?, flow::Transition::Unselect)) + Ok(( + Response::ok().tag(tag).message("CLOSE completed").build()?, + flow::Transition::Unselect, + )) + } + + pub async fn fetch( + self, + sequence_set: &SequenceSet, + attributes: &MacroOrMessageDataItemNames<'a>, + uid: &bool, + ) -> Result<(Response, flow::Transition)> { + match self.mailbox.fetch(sequence_set, attributes, uid).await { + Ok(resp) => Ok(( + Response::ok() + .to_req(self.req) + .message("FETCH completed") + .set_data(resp) + .build()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::no() + .to_req(self.req) + .message(e.to_string()) + .build()?, + flow::Transition::None, + )), + } + } + + pub async fn search( + self, + _charset: &Option>, + _criteria: &SearchKey<'a>, + _uid: &bool, + ) -> Result<(Response, flow::Transition)> { + Ok(( + Response::bad() + .to_req(self.req) + .message("Not implemented") + .build()?, + flow::Transition::None, + )) + } + + pub async fn noop(self) -> Result<(Response, flow::Transition)> { + self.mailbox.mailbox.force_sync().await?; + + let updates = self.mailbox.update().await?; + Ok(( + Response::ok() + .to_req(self.req) + .message("NOOP completed.") + .set_data(updates) + .build()?, + flow::Transition::None, + )) } async fn expunge(self) -> Result<(Response, flow::Transition)> { + let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; Ok(( - Response::ok("EXPUNGE completed")?.with_body(data), + Response::ok() + .tag(tag) + .message("EXPUNGE completed") + .data(data) + .build()?, flow::Transition::None, )) } @@ -74,7 +149,7 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, kind: &StoreType, response: &StoreResponse, - flags: &[Flag], + flags: &[Flag<'a>], uid: &bool, ) -> Result<(Response, flow::Transition)> { let data = self @@ -83,7 +158,11 @@ impl<'a> SelectedContext<'a> { .await?; Ok(( - Response::ok("STORE completed")?.with_body(data), + Response::ok() + .to_req(self.req) + .message("STORE completed") + .set_data(data) + .build()?, flow::Transition::None, )) } @@ -91,18 +170,21 @@ impl<'a> SelectedContext<'a> { async fn copy( self, sequence_set: &SequenceSet, - mailbox: &MailboxCodec, + mailbox: &MailboxCodec<'a>, uid: &bool, ) -> Result<(Response, flow::Transition)> { - let name = String::try_from(mailbox.clone())?; + let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; let mb = match mb_opt { Some(mb) => mb, None => { return Ok(( - Response::no("Destination mailbox does not exist")? - .with_extra_code(Code::TryCreate), + Response::no() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .build()?, flow::Transition::None, )) } @@ -126,10 +208,13 @@ impl<'a> SelectedContext<'a> { ); Ok(( - Response::ok("COPY completed")?.with_extra_code(Code::Other( - "COPYUID".try_into().unwrap(), - Some(copyuid_str), - )), + Response::ok() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .build()?, flow::Transition::None, )) } From 9a8d4c651e5993f09f54cf7c1eacf7a4839ea9db Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 15:35:23 +0100 Subject: [PATCH 13/18] commands now use imap-flow --- src/imap/command/anonymous.rs | 20 +- src/imap/command/anystate.rs | 27 +- src/imap/command/authenticated.rs | 150 +++++----- src/imap/command/examined.rs | 40 +-- src/imap/command/selected.rs | 60 ++-- src/imap/mailbox_view.rs | 441 +++++++++++++++++------------- src/imap/response.rs | 121 ++++---- 7 files changed, 468 insertions(+), 391 deletions(-) diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 42e2a87..4de5fbd 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -13,11 +13,11 @@ use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Command<'static>, + pub req: &'a Command<'a>, pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), @@ -39,14 +39,14 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response, flow::Tran //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Response, flow::Transition)> { + async fn capability(self) -> Result<(Response<'a>, flow::Transition)> { let capabilities: NonEmptyVec = (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; - let res = Response::ok() + let res = Response::build() .to_req(self.req) .message("Server capabilities") .data(Data::Capability(capabilities)) - .build()?; + .ok()?; Ok((res, flow::Transition::None)) } @@ -54,7 +54,7 @@ impl<'a> AnonymousContext<'a> { self, username: &AString<'a>, password: &Secret>, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let (u, p) = ( std::str::from_utf8(username.as_ref())?, std::str::from_utf8(password.declassify().as_ref())?, @@ -65,10 +65,10 @@ impl<'a> AnonymousContext<'a> { Err(e) => { tracing::debug!(error=%e, "authentication failed"); return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Authentication failed") - .build()?, + .no()?, flow::Transition::None, )); } @@ -79,10 +79,10 @@ impl<'a> AnonymousContext<'a> { tracing::info!(username=%u, "connected"); Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("Completed") - .build()?, + .ok()?, flow::Transition::Authenticate(user), )) } diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 2d10ad8..ea3bc16 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -5,45 +5,48 @@ use imap_codec::imap_types::response::{Capability, Data}; use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag) -> Result<(Response, flow::Transition)> { +pub(crate) fn capability<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { let capabilities: NonEmptyVec = (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; - let res = Response::ok() + let res = Response::build() .tag(tag) .message("Server capabilities") .data(Data::Capability(capabilities)) - .build()?; + .ok()?; Ok((res, flow::Transition::None)) } -pub(crate) fn noop_nothing(tag: Tag) -> Result<(Response, flow::Transition)> { +pub(crate) fn noop_nothing<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::ok().tag(tag).message("Noop completed.").build()?, + Response::build().tag(tag).message("Noop completed.").ok()?, flow::Transition::None, )) } -pub(crate) fn logout() -> Result<(Response, flow::Transition)> { +pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> { Ok((Response::bye()?, flow::Transition::Logout)) } -pub(crate) fn not_implemented(tag: Tag, what: &str) -> Result<(Response, flow::Transition)> { +pub(crate) fn not_implemented<'a>( + tag: Tag<'a>, + what: &str, +) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .tag(tag) .message(format!("Command not implemented {}", what)) - .build()?, + .bad()?, flow::Transition::None, )) } -pub(crate) fn wrong_state(tag: Tag) -> Result<(Response, flow::Transition)> { +pub(crate) fn wrong_state<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .tag(tag) .message("Command not authorized in this state") - .build()?, + .bad()?, flow::Transition::None, )) } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index ca4ad03..c9f9ff7 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -20,14 +20,14 @@ use crate::mail::uidindex::*; use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW}; use crate::mail::IMF; -static MAILBOX_HIERARCHY_DELIMITER: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); - pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'static>, + pub req: &'a Command<'a>, pub user: &'a Arc, } -pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>( + ctx: AuthenticatedContext<'a>, +) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), @@ -68,14 +68,14 @@ pub async fn dispatch(ctx: AuthenticatedContext<'_>) -> Result<(Response, flow:: // --- PRIVATE --- impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name = match mailbox { MailboxCodec::Inbox => { return Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("Cannot create INBOX") - .build()?, + .bad()?, flow::Transition::None, )); } @@ -84,38 +84,38 @@ impl<'a> AuthenticatedContext<'a> { match self.user.create_mailbox(&name).await { Ok(()) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("CREATE complete") - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(&e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } } - async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; match self.user.delete_mailbox(&name).await { Ok(()) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("DELETE complete") - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -125,23 +125,23 @@ impl<'a> AuthenticatedContext<'a> { self, from: &MailboxCodec<'a>, to: &MailboxCodec<'a>, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(from).try_into()?; let new_name: &str = MailboxName(to).try_into()?; match self.user.rename_mailbox(&name, &new_name).await { Ok(()) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("RENAME complete") - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -152,14 +152,16 @@ impl<'a> AuthenticatedContext<'a> { reference: &MailboxCodec<'a>, mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { + let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); + let reference: &str = MailboxName(reference).try_into()?; if !reference.is_empty() { return Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("References not supported") - .build()?, + .bad()?, flow::Transition::None, )); } @@ -172,28 +174,28 @@ impl<'a> AuthenticatedContext<'a> { if wildcard.is_empty() { if is_lsub { return Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("LSUB complete") .data(Data::Lsub { items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox: "".try_into().unwrap(), }) - .build()?, + .ok()?, flow::Transition::None, )); } else { return Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("LIST complete") .data(Data::List { items: vec![], - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox: "".try_into().unwrap(), }) - .build()?, + .ok()?, flow::Transition::None, )); } @@ -227,13 +229,13 @@ impl<'a> AuthenticatedContext<'a> { if is_lsub { ret.push(Data::Lsub { items, - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox, }); } else { ret.push(Data::List { items, - delimiter: Some(MAILBOX_HIERARCHY_DELIMITER), + delimiter: Some(mbx_hier_delim), mailbox, }); } @@ -246,11 +248,11 @@ impl<'a> AuthenticatedContext<'a> { "LIST completed" }; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message(msg) - .set_data(ret) - .build()?, + .many_data(ret) + .ok()?, flow::Transition::None, )) } @@ -259,23 +261,23 @@ impl<'a> AuthenticatedContext<'a> { self, mailbox: &MailboxCodec<'a>, attributes: &[StatusDataItemName], - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(name).await?; let mb = match mb_opt { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Mailbox does not exist") - .build()?, + .no()?, flow::Transition::None, )) } }; - let (view, _data) = MailboxView::new(mb).await?; + let view = MailboxView::new(mb).await; let mut ret_attrs = vec![]; for attr in attributes.iter() { @@ -302,57 +304,63 @@ impl<'a> AuthenticatedContext<'a> { }; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("STATUS completed") .data(data) - .build()?, + .ok()?, flow::Transition::None, )) } - async fn subscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn subscribe( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("SUBSCRIBE complete") - .build()?, + .ok()?, flow::Transition::None, )) } else { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message(format!("Mailbox {} does not exist", name)) - .build()?, + .bad()?, flow::Transition::None, )) } } - async fn unsubscribe(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn unsubscribe( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message(format!( "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", name )) - .build()?, + .bad()?, flow::Transition::None, )) } else { Ok(( - Response::no() + Response::build() .to_req(self.req) .message(format!("Mailbox {} does not exist", name)) - .build()?, + .no()?, flow::Transition::None, )) } @@ -391,7 +399,7 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -399,29 +407,30 @@ impl<'a> AuthenticatedContext<'a> { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Mailbox does not exist") - .build()?, + .no()?, flow::Transition::None, )) } }; tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); - let (mb, data) = MailboxView::new(mb).await?; + let mb = MailboxView::new(mb).await; + let data = mb.summary()?; Ok(( - Response::ok() + Response::build() .message("Select completed") .code(Code::ReadWrite) - .data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::Select(mb), )) } - async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response, flow::Transition)> { + async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -429,25 +438,26 @@ impl<'a> AuthenticatedContext<'a> { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Mailbox does not exist") - .build()?, + .no()?, flow::Transition::None, )) } }; tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); - let (mb, data) = MailboxView::new(mb).await?; + let mb = MailboxView::new(mb).await; + let data = mb.summary()?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("Examine completed") .code(Code::ReadOnly) - .data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::Examine(mb), )) } @@ -458,24 +468,24 @@ impl<'a> AuthenticatedContext<'a> { flags: &[Flag<'a>], date: &Option, message: &Literal<'a>, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { Ok((_mb, uidvalidity, uid)) => Ok(( - Response::ok() + Response::build() .tag(append_tag) .message("APPEND completed") .code(Code::Other(CodeOther::unvalidated( format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), ))) - .build()?, + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .tag(append_tag) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index cab3fdd..7f9c39c 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -19,7 +19,7 @@ pub struct ExaminedContext<'a> { pub mailbox: &'a mut MailboxView, } -pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state @@ -41,10 +41,10 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans } => ctx.search(charset, criteria, uid).await, CommandBody::Noop | CommandBody::Check => ctx.noop().await, CommandBody::Expunge { .. } | CommandBody::Store { .. } => Ok(( - Response::bad() + Response::build() .to_req(ctx.req) .message("Forbidden command: can't write in read-only mode (EXAMINE)") - .build()?, + .bad()?, flow::Transition::None, )), @@ -58,12 +58,12 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response, flow::Trans impl<'a> ExaminedContext<'a> { /// CLOSE in examined state is not the same as in selected state /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn close(self) -> Result<(Response, flow::Transition)> { + async fn close(self) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("CLOSE completed") - .build()?, + .ok()?, flow::Transition::Unselect, )) } @@ -71,23 +71,23 @@ impl<'a> ExaminedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("FETCH completed") - .set_data(resp) - .build()?, + .set_body(resp) + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -98,26 +98,26 @@ impl<'a> ExaminedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("Not implemented") - .build()?, + .bad()?, flow::Transition::None, )) } - pub async fn noop(self) -> Result<(Response, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("NOOP completed.") - .set_data(updates) - .build()?, + .set_body(updates) + .ok()?, flow::Transition::None, )) } diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 148901d..cd5d221 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -23,7 +23,7 @@ pub struct SelectedContext<'a> { pub mailbox: &'a mut MailboxView, } -pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Transition)> { +pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state @@ -65,13 +65,13 @@ pub async fn dispatch(ctx: SelectedContext<'_>) -> Result<(Response, flow::Trans // --- PRIVATE --- impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response, flow::Transition)> { + async fn close(self) -> Result<(Response<'a>, flow::Transition)> { // We expunge messages, // but we don't send the untagged EXPUNGE responses let tag = self.req.tag.clone(); self.expunge().await?; Ok(( - Response::ok().tag(tag).message("CLOSE completed").build()?, + Response::build().tag(tag).message("CLOSE completed").ok()?, flow::Transition::Unselect, )) } @@ -79,23 +79,23 @@ impl<'a> SelectedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("FETCH completed") - .set_data(resp) - .build()?, + .set_body(resp) + .ok()?, flow::Transition::None, )), Err(e) => Ok(( - Response::no() + Response::build() .to_req(self.req) .message(e.to_string()) - .build()?, + .no()?, flow::Transition::None, )), } @@ -106,40 +106,40 @@ impl<'a> SelectedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { Ok(( - Response::bad() + Response::build() .to_req(self.req) .message("Not implemented") - .build()?, + .bad()?, flow::Transition::None, )) } - pub async fn noop(self) -> Result<(Response, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("NOOP completed.") - .set_data(updates) - .build()?, + .set_body(updates) + .ok()?, flow::Transition::None, )) } - async fn expunge(self) -> Result<(Response, flow::Transition)> { + async fn expunge(self) -> Result<(Response<'a>, flow::Transition)> { let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; Ok(( - Response::ok() + Response::build() .tag(tag) .message("EXPUNGE completed") - .data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::None, )) } @@ -151,18 +151,18 @@ impl<'a> SelectedContext<'a> { response: &StoreResponse, flags: &[Flag<'a>], uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let data = self .mailbox .store(sequence_set, kind, response, flags, uid) .await?; Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("STORE completed") - .set_data(data) - .build()?, + .set_body(data) + .ok()?, flow::Transition::None, )) } @@ -172,7 +172,7 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, mailbox: &MailboxCodec<'a>, uid: &bool, - ) -> Result<(Response, flow::Transition)> { + ) -> Result<(Response<'a>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -180,11 +180,11 @@ impl<'a> SelectedContext<'a> { Some(mb) => mb, None => { return Ok(( - Response::no() + Response::build() .to_req(self.req) .message("Destination mailbox does not exist") .code(Code::TryCreate) - .build()?, + .no()?, flow::Transition::None, )) } @@ -208,13 +208,13 @@ impl<'a> SelectedContext<'a> { ); Ok(( - Response::ok() + Response::build() .to_req(self.req) .message("COPY completed") .code(Code::Other(CodeOther::unvalidated( format!("COPYUID {}", copyuid_str).into_bytes(), ))) - .build()?, + .ok()?, flow::Transition::None, )) } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index d9baf47..2e5444b 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -4,21 +4,19 @@ use std::num::NonZeroU32; use std::sync::Arc; use anyhow::{anyhow, bail, Error, Result}; -use boitalettres::proto::res::body::Data as Body; use chrono::{Offset, TimeZone, Utc}; use futures::stream::{FuturesOrdered, StreamExt}; -use imap_codec::imap_types::address::Address; use imap_codec::imap_types::body::{BasicFields, Body as FetchBody, BodyStructure, SpecificFields}; -use imap_codec::imap_types::core::{AString, Atom, IString, NString}; -use imap_codec::imap_types::datetime::MyDateTime; -use imap_codec::imap_types::envelope::Envelope; -use imap_codec::imap_types::fetch_attributes::{ - FetchAttribute, MacroOrFetchAttributes, Section as FetchSection, +use imap_codec::imap_types::core::{AString, Atom, IString, NString, NonEmptyVec}; +use imap_codec::imap_types::datetime::DateTime; +use imap_codec::imap_types::envelope::{Address, Envelope}; +use imap_codec::imap_types::fetch::{ + MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName, Section as FetchSection, }; -use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::imap_types::response::{Code, Data, MessageAttribute, Status}; +use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; +use imap_codec::imap_types::response::{Code, Data, Status}; use imap_codec::imap_types::sequence::{self, SequenceSet}; use eml_codec::{ @@ -28,6 +26,7 @@ use eml_codec::{ }; use crate::cryptoblob::Key; +use crate::imap::response::Body; use crate::mail::mailbox::{MailMeta, Mailbox}; use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex}; use crate::mail::unique_ident::UniqueIdent; @@ -76,20 +75,20 @@ impl<'a> FetchedMail<'a> { } } -pub struct AttributesProxy { - attrs: Vec, +pub struct AttributesProxy<'a> { + attrs: Vec>, } -impl AttributesProxy { - fn new(attrs: &MacroOrFetchAttributes, is_uid_fetch: bool) -> Self { +impl<'a> AttributesProxy<'a> { + fn new(attrs: &'a MacroOrMessageDataItemNames<'a>, is_uid_fetch: bool) -> Self { // Expand macros let mut fetch_attrs = match attrs { - MacroOrFetchAttributes::Macro(m) => m.expand(), - MacroOrFetchAttributes::FetchAttributes(a) => a.clone(), + MacroOrMessageDataItemNames::Macro(m) => m.expand(), + MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(), }; // Handle uids - if is_uid_fetch && !fetch_attrs.contains(&FetchAttribute::Uid) { - fetch_attrs.push(FetchAttribute::Uid); + if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) { + fetch_attrs.push(MessageDataItemName::Uid); } Self { attrs: fetch_attrs } @@ -99,11 +98,11 @@ impl AttributesProxy { self.attrs.iter().any(|x| { matches!( x, - FetchAttribute::Body - | FetchAttribute::BodyExt { .. } - | FetchAttribute::Rfc822 - | FetchAttribute::Rfc822Text - | FetchAttribute::BodyStructure + MessageDataItemName::Body + | MessageDataItemName::BodyExt { .. } + | MessageDataItemName::Rfc822 + | MessageDataItemName::Rfc822Text + | MessageDataItemName::BodyStructure ) }) } @@ -127,16 +126,20 @@ pub struct MailView<'a> { meta: &'a MailMeta, flags: &'a Vec, content: FetchedMail<'a>, - add_seen: bool, +} + +enum SeenFlag { + DoNothing, + MustAdd, } impl<'a> MailView<'a> { - fn uid(&self) -> MessageAttribute { - MessageAttribute::Uid(self.ids.uid) + fn uid(&self) -> MessageDataItem<'static> { + MessageDataItem::Uid(self.ids.uid.clone()) } - fn flags(&self) -> MessageAttribute { - MessageAttribute::Flags( + fn flags(&self) -> MessageDataItem<'static> { + MessageDataItem::Flags( self.flags .iter() .filter_map(|f| string_to_flag(f)) @@ -144,12 +147,12 @@ impl<'a> MailView<'a> { ) } - fn rfc_822_size(&self) -> MessageAttribute { - MessageAttribute::Rfc822Size(self.meta.rfc822_size as u32) + fn rfc_822_size(&self) -> MessageDataItem<'static> { + MessageDataItem::Rfc822Size(self.meta.rfc822_size as u32) } - fn rfc_822_header(&self) -> MessageAttribute { - MessageAttribute::Rfc822Header(NString( + fn rfc_822_header(&self) -> MessageDataItem<'static> { + MessageDataItem::Rfc822Header(NString( self.meta .headers .to_vec() @@ -159,41 +162,42 @@ impl<'a> MailView<'a> { )) } - fn rfc_822_text(&self) -> Result { - Ok(MessageAttribute::Rfc822Text(NString( + fn rfc_822_text(&self) -> Result> { + Ok(MessageDataItem::Rfc822Text(NString( self.content .as_full()? .raw_body + .to_vec() .try_into() .ok() .map(IString::Literal), ))) } - fn rfc822(&self) -> Result { - Ok(MessageAttribute::Rfc822(NString( + fn rfc822(&self) -> Result> { + Ok(MessageDataItem::Rfc822(NString( self.content .as_full()? .raw_part - .clone() + .to_vec() .try_into() .ok() .map(IString::Literal), ))) } - fn envelope(&self) -> MessageAttribute { - MessageAttribute::Envelope(message_envelope(self.content.imf())) + fn envelope(&self) -> MessageDataItem<'static> { + MessageDataItem::Envelope(message_envelope(self.content.imf().clone())) } - fn body(&self) -> Result { - Ok(MessageAttribute::Body(build_imap_email_struct( + fn body(&self) -> Result> { + Ok(MessageDataItem::Body(build_imap_email_struct( self.content.as_full()?.child.as_ref(), )?)) } - fn body_structure(&self) -> Result { - Ok(MessageAttribute::Body(build_imap_email_struct( + fn body_structure(&self) -> Result> { + Ok(MessageDataItem::Body(build_imap_email_struct( self.content.as_full()?.child.as_ref(), )?)) } @@ -202,12 +206,14 @@ impl<'a> MailView<'a> { /// peek does not implicitly set the \Seen flag /// eg. BODY[HEADER.FIELDS (DATE FROM)] /// eg. BODY[]<0.2048> - fn body_ext( - &mut self, - section: &Option, + fn body_ext<'b>( + &self, + section: &Option>, partial: &Option<(u32, NonZeroU32)>, peek: &bool, - ) -> Result { + ) -> Result<(MessageDataItem<'b>, SeenFlag)> { + let mut seen = SeenFlag::DoNothing; + // Extract message section let text = get_message_section(self.content.as_anypart()?, section)?; @@ -215,7 +221,7 @@ impl<'a> MailView<'a> { if !peek && !self.flags.iter().any(|x| *x == seen_flag) { // Add \Seen flag //self.mailbox.add_flags(uuid, &[seen_flag]).await?; - self.add_seen = true; + seen = SeenFlag::MustAdd; } // Handle <> which cut the message bytes @@ -223,49 +229,60 @@ impl<'a> MailView<'a> { let data = NString(text.to_vec().try_into().ok().map(IString::Literal)); - return Ok(MessageAttribute::BodyExt { - section: section.clone(), - origin, - data, - }); + return Ok(( + MessageDataItem::BodyExt { + section: section.as_ref().map(|fs| fs.clone()), + origin, + data, + }, + seen, + )); } - fn internal_date(&self) -> Result { + fn internal_date(&self) -> Result> { let dt = Utc .fix() .timestamp_opt(i64::try_from(self.meta.internaldate / 1000)?, 0) .earliest() .ok_or(anyhow!("Unable to parse internal date"))?; - Ok(MessageAttribute::InternalDate(MyDateTime(dt))) + Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) } - fn filter(&mut self, ap: &AttributesProxy) -> Result { + fn filter<'b>(&self, ap: &AttributesProxy<'b>) -> Result<(Body<'b>, SeenFlag)> { + let mut seen = SeenFlag::DoNothing; let res_attrs = ap .attrs .iter() .map(|attr| match attr { - FetchAttribute::Uid => Ok(self.uid()), - FetchAttribute::Flags => Ok(self.flags()), - FetchAttribute::Rfc822Size => Ok(self.rfc_822_size()), - FetchAttribute::Rfc822Header => Ok(self.rfc_822_header()), - FetchAttribute::Rfc822Text => self.rfc_822_text(), - FetchAttribute::Rfc822 => self.rfc822(), - FetchAttribute::Envelope => Ok(self.envelope()), - FetchAttribute::Body => self.body(), - FetchAttribute::BodyStructure => self.body_structure(), - FetchAttribute::BodyExt { + MessageDataItemName::Uid => Ok(self.uid()), + MessageDataItemName::Flags => Ok(self.flags()), + MessageDataItemName::Rfc822Size => Ok(self.rfc_822_size()), + MessageDataItemName::Rfc822Header => Ok(self.rfc_822_header()), + MessageDataItemName::Rfc822Text => self.rfc_822_text(), + MessageDataItemName::Rfc822 => self.rfc822(), + MessageDataItemName::Envelope => Ok(self.envelope()), + MessageDataItemName::Body => self.body(), + MessageDataItemName::BodyStructure => self.body_structure(), + MessageDataItemName::BodyExt { section, partial, peek, - } => self.body_ext(section, partial, peek), - FetchAttribute::InternalDate => self.internal_date(), + } => { + let (body, has_seen) = self.body_ext(section, partial, peek)?; + seen = has_seen; + Ok(body) + } + MessageDataItemName::InternalDate => self.internal_date(), }) .collect::, _>>()?; - Ok(Body::Data(Data::Fetch { - seq_or_uid: self.ids.i, - attributes: res_attrs, - })) + Ok(( + Body::Data(Data::Fetch { + seq: self.ids.i, + items: res_attrs.try_into()?, + }), + seen, + )) } } @@ -376,7 +393,6 @@ impl<'a> MailSelectionBuilder<'a> { meta, flags, content, - add_seen: false, }) .collect()) } @@ -396,35 +412,26 @@ pub struct MailboxView { impl MailboxView { /// Creates a new IMAP view into a mailbox. - /// Generates the necessary IMAP messages so that the client - /// has a satisfactory summary of the current mailbox's state. - /// These are the messages that are sent in response to a SELECT command. - pub async fn new(mailbox: Arc) -> Result<(Self, Vec)> { + pub async fn new(mailbox: Arc) -> Self { let state = mailbox.current_uid_index().await; - let new_view = Self { + Self { mailbox, known_state: state, - }; - - let mut data = Vec::::new(); - data.push(new_view.exists_status()?); - data.push(new_view.recent_status()?); - data.extend(new_view.flags_status()?.into_iter()); - data.push(new_view.uidvalidity_status()?); - data.push(new_view.uidnext_status()?); - - Ok((new_view, data)) + } } + /// Create an updated view, useful to make a diff + /// between what the client knows and new stuff /// Produces a set of IMAP responses describing the change between /// what the client knows and what is actually in the mailbox. /// This does NOT trigger a sync, it bases itself on what is currently /// loaded in RAM by Bayou. - pub async fn update(&mut self) -> Result> { - let new_view = MailboxView { - mailbox: self.mailbox.clone(), - known_state: self.mailbox.current_uid_index().await, + pub async fn update(&mut self) -> Result>> { + let old_view: &mut Self = self; + let new_view = Self { + mailbox: old_view.mailbox.clone(), + known_state: old_view.mailbox.current_uid_index().await, }; let mut data = Vec::::new(); @@ -446,7 +453,7 @@ impl MailboxView { // - notify client of expunged mails let mut n_expunge = 0; - for (i, (_uid, uuid)) in self.known_state.idx_by_uid.iter().enumerate() { + for (i, (_uid, uuid)) in old_view.known_state.idx_by_uid.iter().enumerate() { if !new_view.known_state.table.contains_key(uuid) { data.push(Body::Data(Data::Expunge( NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), @@ -456,49 +463,63 @@ impl MailboxView { } // - if new mails arrived, notify client of number of existing mails - if new_view.known_state.table.len() != self.known_state.table.len() - n_expunge - || new_view.known_state.uidvalidity != self.known_state.uidvalidity + if new_view.known_state.table.len() != old_view.known_state.table.len() - n_expunge + || new_view.known_state.uidvalidity != old_view.known_state.uidvalidity { data.push(new_view.exists_status()?); } - if new_view.known_state.uidvalidity != self.known_state.uidvalidity { + if new_view.known_state.uidvalidity != old_view.known_state.uidvalidity { // TODO: do we want to push less/more info than this? data.push(new_view.uidvalidity_status()?); data.push(new_view.uidnext_status()?); } else { // - if flags changed for existing mails, tell client for (i, (_uid, uuid)) in new_view.known_state.idx_by_uid.iter().enumerate() { - let old_mail = self.known_state.table.get(uuid); + let old_mail = old_view.known_state.table.get(uuid); let new_mail = new_view.known_state.table.get(uuid); if old_mail.is_some() && old_mail != new_mail { if let Some((uid, flags)) = new_mail { data.push(Body::Data(Data::Fetch { - seq_or_uid: NonZeroU32::try_from((i + 1) as u32).unwrap(), - attributes: vec![ - MessageAttribute::Uid(*uid), - MessageAttribute::Flags( + seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), + items: vec![ + MessageDataItem::Uid(*uid), + MessageDataItem::Flags( flags.iter().filter_map(|f| string_to_flag(f)).collect(), ), - ], + ] + .try_into()?, })); } } } } - - *self = new_view; + *old_view = new_view; Ok(data) } - pub async fn store( + /// Generates the necessary IMAP messages so that the client + /// has a satisfactory summary of the current mailbox's state. + /// These are the messages that are sent in response to a SELECT command. + pub fn summary(&self) -> Result>> { + let mut data = Vec::::new(); + data.push(self.exists_status()?); + data.push(self.recent_status()?); + data.extend(self.flags_status()?.into_iter()); + data.push(self.uidvalidity_status()?); + data.push(self.uidnext_status()?); + + Ok(data) + } + + pub async fn store<'a>( &mut self, sequence_set: &SequenceSet, kind: &StoreType, _response: &StoreResponse, - flags: &[Flag], + flags: &[Flag<'a>], is_uid_store: &bool, - ) -> Result> { + ) -> Result>> { self.mailbox.opportunistic_sync().await?; let flags = flags.iter().map(|x| x.to_string()).collect::>(); @@ -522,7 +543,7 @@ impl MailboxView { self.update().await } - pub async fn expunge(&mut self) -> Result> { + pub async fn expunge(&mut self) -> Result>> { self.mailbox.opportunistic_sync().await?; let deleted_flag = Flag::Deleted.to_string(); @@ -569,12 +590,12 @@ impl MailboxView { /// Looks up state changes in the mailbox and produces a set of IMAP /// responses describing the new state. - pub async fn fetch( + pub async fn fetch<'b>( &self, sequence_set: &SequenceSet, - attributes: &MacroOrFetchAttributes, + attributes: &'b MacroOrMessageDataItemNames<'b>, is_uid_fetch: &bool, - ) -> Result> { + ) -> Result>> { let ap = AttributesProxy::new(attributes, *is_uid_fetch); // Prepare data @@ -619,31 +640,37 @@ impl MailboxView { selection.with_bodies(bodies.as_slice()); // Build mail selection views - let mut views = selection.build()?; + let views = selection.build()?; // Filter views to build the result - let ret = views - .iter_mut() - .filter_map(|mv| mv.filter(&ap).ok()) - .collect::>(); - - // Register seen flags - let future_flags = views + // Also identify what must be put as seen + let filtered_view = views .iter() - .filter(|mv| mv.add_seen) - .map(|mv| async move { + .filter_map(|mv| mv.filter(&ap).ok().map(|(body, seen)| (mv, body, seen))) + .collect::>(); + // Register seen flags + let future_flags = filtered_view + .iter() + .filter(|(_mv, _body, seen)| matches!(seen, SeenFlag::MustAdd)) + .map(|(mv, _body, _seen)| async move { let seen_flag = Flag::Seen.to_string(); self.mailbox.add_flags(mv.ids.uuid, &[seen_flag]).await?; Ok::<_, anyhow::Error>(()) }) .collect::>(); + future_flags .collect::>() .await .into_iter() .collect::>()?; - Ok(ret) + let command_body = filtered_view + .into_iter() + .map(|(_mv, body, _seen)| body) + .collect::>(); + + Ok(command_body) } // ---- @@ -717,7 +744,7 @@ impl MailboxView { // ---- /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` - fn uidvalidity_status(&self) -> Result { + fn uidvalidity_status(&self) -> Result> { let uid_validity = Status::ok( None, Some(Code::UidValidity(self.uidvalidity())), @@ -732,7 +759,7 @@ impl MailboxView { } /// Produce an OK [UIDNEXT _] message corresponding to `known_state` - fn uidnext_status(&self) -> Result { + fn uidnext_status(&self) -> Result> { let next_uid = Status::ok( None, Some(Code::UidNext(self.uidnext())), @@ -748,7 +775,7 @@ impl MailboxView { /// Produce an EXISTS message corresponding to the number of mails /// in `known_state` - fn exists_status(&self) -> Result { + fn exists_status(&self) -> Result> { Ok(Body::Data(Data::Exists(self.exists()?))) } @@ -758,7 +785,7 @@ impl MailboxView { /// Produce a RECENT message corresponding to the number of /// recent mails in `known_state` - fn recent_status(&self) -> Result { + fn recent_status(&self) -> Result> { Ok(Body::Data(Data::Recent(self.recent()?))) } @@ -774,27 +801,48 @@ impl MailboxView { /// Produce a FLAGS and a PERMANENTFLAGS message that indicates /// the flags that are in `known_state` + default flags - fn flags_status(&self) -> Result> { - let mut flags: Vec = self + fn flags_status(&self) -> Result>> { + let mut body = vec![]; + + // 1. Collecting all the possible flags in the mailbox + // 1.a Fetch them from our index + let mut known_flags: Vec = self .known_state .idx_by_flag .flags() - .filter_map(|f| string_to_flag(f)) + .filter_map(|f| match string_to_flag(f) { + Some(FlagFetch::Flag(fl)) => Some(fl), + _ => None, + }) .collect(); + // 1.b Merge it with our default flags list for f in DEFAULT_FLAGS.iter() { - if !flags.contains(f) { - flags.push(f.clone()); + if !known_flags.contains(f) { + known_flags.push(f.clone()); } } - let mut ret = vec![Body::Data(Data::Flags(flags.clone()))]; + // 1.c Create the IMAP message + body.push(Body::Data(Data::Flags(known_flags.clone()))); - flags.push(Flag::Permanent); - let permanent_flags = - Status::ok(None, Some(Code::PermanentFlags(flags)), "Flags permitted") - .map_err(Error::msg)?; - ret.push(Body::Status(permanent_flags)); + // 2. Returning flags that are persisted + // 2.a Always advertise our default flags + let mut permanent = DEFAULT_FLAGS + .iter() + .map(|f| FlagPerm::Flag(f.clone())) + .collect::>(); + // 2.b Say that we support any keyword flag + permanent.push(FlagPerm::Asterisk); + // 2.c Create the IMAP message + let permanent_flags = Status::ok( + None, + Some(Code::PermanentFlags(permanent)), + "Flags permitted", + ) + .map_err(Error::msg)?; + body.push(Body::Status(permanent_flags)); - Ok(ret) + // Done! + Ok(body) } pub(crate) fn unseen_count(&self) -> usize { @@ -809,21 +857,21 @@ impl MailboxView { } } -fn string_to_flag(f: &str) -> Option { +fn string_to_flag(f: &str) -> Option> { match f.chars().next() { Some('\\') => match f { - "\\Seen" => Some(Flag::Seen), - "\\Answered" => Some(Flag::Answered), - "\\Flagged" => Some(Flag::Flagged), - "\\Deleted" => Some(Flag::Deleted), - "\\Draft" => Some(Flag::Draft), - "\\Recent" => Some(Flag::Recent), + "\\Seen" => Some(FlagFetch::Flag(Flag::Seen)), + "\\Answered" => Some(FlagFetch::Flag(Flag::Answered)), + "\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)), + "\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)), + "\\Draft" => Some(FlagFetch::Flag(Flag::Draft)), + "\\Recent" => Some(FlagFetch::Recent), _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { Err(_) => { tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); None } - Ok(a) => Some(Flag::Extension(a)), + Ok(a) => Some(FlagFetch::Flag(Flag::system(a))), }, }, Some(_) => match Atom::try_from(f.to_string()) { @@ -831,7 +879,7 @@ fn string_to_flag(f: &str) -> Option { tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); None } - Ok(a) => Some(Flag::Keyword(a)), + Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))), }, None => None, } @@ -858,7 +906,7 @@ fn string_to_flag(f: &str) -> Option { //@FIXME return an error if the envelope is invalid instead of panicking //@FIXME some fields must be defaulted if there are not set. -fn message_envelope(msg: &imf::Imf) -> Envelope { +fn message_envelope(msg: &imf::Imf) -> Envelope<'static> { let from = msg.from.iter().map(convert_mbx).collect::>(); Envelope { @@ -900,7 +948,7 @@ fn message_envelope(msg: &imf::Imf) -> Envelope { } } -fn convert_addresses(addrlist: &Vec) -> Vec
{ +fn convert_addresses(addrlist: &Vec) -> Vec> { let mut acc = vec![]; for item in addrlist { match item { @@ -911,23 +959,23 @@ fn convert_addresses(addrlist: &Vec) -> Vec
{ return acc; } -fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address { - Address::new( - NString( +fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> { + Address { + name: NString( addr.name .as_ref() .map(|x| IString::try_from(x.to_string()).unwrap()), ), // SMTP at-domain-list (source route) seems obsolete since at least 1991 // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html - NString(None), - NString(Some( + adl: NString(None), + mailbox: NString(Some( IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), )), - NString(Some( + host: NString(Some( IString::try_from(addr.addrspec.domain.to_string()).unwrap(), )), - ) + } } /* @@ -945,19 +993,23 @@ b fetch 29878:29879 (BODY) b OK Fetch completed (0.001 + 0.000 secs). */ -fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result { +fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result> { match part { AnyPart::Mult(x) => { let itype = &x.mime.interpreted_type; let subtype = IString::try_from(itype.subtype.to_string()) .unwrap_or(unchecked_istring("alternative")); + let inner_bodies = x + .children + .iter() + .filter_map(|inner| build_imap_email_struct(&inner).ok()) + .collect::>(); + NonEmptyVec::validate(&inner_bodies)?; + let bodies = NonEmptyVec::unvalidated(inner_bodies); + Ok(BodyStructure::Multi { - bodies: x - .children - .iter() - .filter_map(|inner| build_imap_email_struct(&inner).ok()) - .collect(), + bodies, subtype, extension_data: None, /*Some(MultipartExtensionData { @@ -996,7 +1048,7 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result { number_of_lines: nol(x.body), }, }, - extension: None, + extension_data: None, }) } AnyPart::Bin(x) => { @@ -1009,9 +1061,10 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result { }; let ct = x.mime.fields.ctype.as_ref().unwrap_or(&default); - let type_ = IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type type given"), - ))?; + let r#type = + IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( + anyhow!("Unable to build IString from given Content-Type type given"), + ))?; let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string()).or(Err(anyhow!( @@ -1021,9 +1074,9 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result { Ok(BodyStructure::Single { body: FetchBody { basic, - specific: SpecificFields::Basic { type_, subtype }, + specific: SpecificFields::Basic { r#type, subtype }, }, - extension: None, + extension_data: None, }) } AnyPart::Msg(x) => { @@ -1033,12 +1086,12 @@ fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result { body: FetchBody { basic, specific: SpecificFields::Message { - envelope: message_envelope(&x.imf), + envelope: Box::new(message_envelope(&x.imf)), body_structure: Box::new(build_imap_email_struct(x.child.as_ref())?), number_of_lines: nol(x.raw_part), }, }, - extension: None, + extension_data: None, }) } } @@ -1059,7 +1112,7 @@ fn unchecked_istring(s: &'static str) -> IString { IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") } -fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result { +fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result> { let parameter_list = m .ctype .as_ref() @@ -1136,20 +1189,18 @@ fn get_message_section<'a>( .ok_or(anyhow!("Part must be a message"))?; match section { Some(FetchSection::Text(None)) => Ok(msg.raw_body.into()), - Some(FetchSection::Text(Some(part))) => { - map_subpart(parsed, part.0.as_slice(), |part_msg| { - Ok(part_msg - .as_message() - .ok_or(Error::msg( - "Not a message/rfc822 part while expected by request (TEXT)", - ))? - .raw_body - .into()) - }) - } + Some(FetchSection::Text(Some(part))) => map_subpart(parsed, part.0.as_ref(), |part_msg| { + Ok(part_msg + .as_message() + .ok_or(Error::msg( + "Not a message/rfc822 part while expected by request (TEXT)", + ))? + .raw_body + .into()) + }), Some(FetchSection::Header(part)) => map_subpart( parsed, - part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), + part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]), |part_msg| { Ok(part_msg .as_message() @@ -1165,17 +1216,18 @@ fn get_message_section<'a>( ) => { let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _))); let fields = fields + .as_ref() .iter() .map(|x| match x { - AString::Atom(a) => a.as_bytes(), - AString::String(IString::Literal(l)) => l.as_slice(), - AString::String(IString::Quoted(q)) => q.as_bytes(), + AString::Atom(a) => a.inner().as_bytes(), + AString::String(IString::Literal(l)) => l.as_ref(), + AString::String(IString::Quoted(q)) => q.inner().as_bytes(), }) .collect::>(); map_subpart( parsed, - part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), + part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]), |part_msg| { let mut ret = vec![]; for f in &part_msg.mime().kv { @@ -1195,7 +1247,7 @@ fn get_message_section<'a>( }, ) } - Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |part| { + Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_ref(), |part| { let bytes = match &part { AnyPart::Txt(p) => p.body, AnyPart::Bin(p) => p.body, @@ -1204,7 +1256,7 @@ fn get_message_section<'a>( }; Ok(bytes.to_vec().into()) }), - Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_slice(), |part| { + Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_ref(), |part| { let bytes = match &part { AnyPart::Txt(p) => p.mime.fields.raw, AnyPart::Bin(p) => p.mime.fields.raw, @@ -1246,13 +1298,13 @@ mod tests { use crate::cryptoblob; use crate::mail::unique_ident; use imap_codec::codec::Encode; - use imap_codec::imap_types::fetch_attributes::Section; + use imap_codec::imap_types::fetch::Section; use std::fs; #[test] fn mailview_body_ext() -> Result<()> { let ap = AttributesProxy::new( - &MacroOrFetchAttributes::FetchAttributes(vec![FetchAttribute::BodyExt { + &MacroOrMessageDataItemNames::FetchAttributes(vec![MessageDataItemName::BodyExt { section: Some(Section::Header(None)), partial: None, peek: false, @@ -1281,14 +1333,13 @@ mod tests { content, meta: &meta, flags: &flags, - add_seen: false, }; let res_body = mv.filter(&ap)?; let fattr = match res_body { Body::Data(Data::Fetch { - seq_or_uid: _seq, - attributes: attr, + seq: _seq, + items: attr, }) => Ok(attr), _ => Err(anyhow!("Not a fetch body")), }?; @@ -1296,7 +1347,7 @@ mod tests { assert_eq!(fattr.len(), 1); let (sec, _orig, _data) = match &fattr[0] { - MessageAttribute::BodyExt { + MessageDataItemName::BodyExt { section, origin, data, @@ -1349,7 +1400,7 @@ mod tests { let message = eml_codec::parse_message(&txt).unwrap().1; let mut resp = Vec::new(); - MessageAttribute::Body(build_imap_email_struct(&message.child)?) + MessageDataItemName::Body(build_imap_email_struct(&message.child)?) .encode(&mut resp) .unwrap(); diff --git a/src/imap/response.rs b/src/imap/response.rs index 22e91f3..012c8ed 100644 --- a/src/imap/response.rs +++ b/src/imap/response.rs @@ -1,34 +1,26 @@ use anyhow::Result; use imap_codec::imap_types::command::Command; use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::{Code, Data, Status, StatusKind}; +use imap_codec::imap_types::response::{Code, Data, Status}; -pub struct ResponseBuilder { - status: StatusKind, - tag: Option>, - code: Option>, +pub enum Body<'a> { + Data(Data<'a>), + Status(Status<'a>), +} + +pub struct ResponseBuilder<'a> { + tag: Option>, + code: Option>, text: String, - data: Vec>, + body: Vec>, } -impl<'a> Default for ResponseBuilder { - fn default() -> ResponseBuilder { - ResponseBuilder { - status: StatusKind::Bad, - tag: None, - code: None, - text: "".to_string(), - data: vec![], - } - } -} - -impl ResponseBuilder { - pub fn to_req(mut self, cmd: &Command) -> Self { - self.tag = Some(cmd.tag); +impl<'a> ResponseBuilder<'a> { + pub fn to_req(mut self, cmd: &Command<'a>) -> Self { + self.tag = Some(cmd.tag.clone()); self } - pub fn tag(mut self, tag: Tag) -> Self { + pub fn tag(mut self, tag: Tag<'a>) -> Self { self.tag = Some(tag); self } @@ -38,60 +30,81 @@ impl ResponseBuilder { self } - pub fn code(mut self, code: Code) -> Self { + pub fn code(mut self, code: Code<'a>) -> Self { self.code = Some(code); self } - pub fn data(mut self, data: Data) -> Self { - self.data.push(data); + pub fn data(mut self, data: Data<'a>) -> Self { + self.body.push(Body::Data(data)); self } - pub fn set_data(mut self, data: Vec) -> Self { - self.data = data; + pub fn many_data(mut self, data: Vec>) -> Self { + for d in data.into_iter() { + self = self.data(d); + } self } - pub fn build(self) -> Result { + pub fn info(mut self, status: Status<'a>) -> Self { + self.body.push(Body::Status(status)); + self + } + + pub fn many_info(mut self, status: Vec>) -> Self { + for d in status.into_iter() { + self = self.info(d); + } + self + } + + pub fn set_body(mut self, body: Vec>) -> Self { + self.body = body; + self + } + + pub fn ok(self) -> Result> { Ok(Response { - status: Status::new(self.tag, self.status, self.code, self.text)?, - data: self.data, + completion: Status::ok(self.tag, self.code, self.text)?, + body: self.body, + }) + } + + pub fn no(self) -> Result> { + Ok(Response { + completion: Status::no(self.tag, self.code, self.text)?, + body: self.body, + }) + } + + pub fn bad(self) -> Result> { + Ok(Response { + completion: Status::bad(self.tag, self.code, self.text)?, + body: self.body, }) } } -pub struct Response { - data: Vec>, - status: Status<'static>, +pub struct Response<'a> { + body: Vec>, + completion: Status<'a>, } -impl Response { - pub fn ok() -> ResponseBuilder { +impl<'a> Response<'a> { + pub fn build() -> ResponseBuilder<'a> { ResponseBuilder { - status: StatusKind::Ok, - ..ResponseBuilder::default() + tag: None, + code: None, + text: "".to_string(), + body: vec![], } } - pub fn no() -> ResponseBuilder { - ResponseBuilder { - status: StatusKind::No, - ..ResponseBuilder::default() - } - } - - pub fn bad() -> ResponseBuilder { - ResponseBuilder { - status: StatusKind::Bad, - ..ResponseBuilder::default() - } - } - - pub fn bye() -> Result { + pub fn bye() -> Result> { Ok(Response { - status: Status::bye(None, "bye")?, - data: vec![], + completion: Status::bye(None, "bye")?, + body: vec![], }) } } From 0d667a30301bec47c03314ff0e449a220ad3b913 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 20:23:33 +0100 Subject: [PATCH 14/18] compile with imap-flow --- src/imap/command/anonymous.rs | 22 +-- src/imap/command/anystate.rs | 6 +- src/imap/command/authenticated.rs | 40 ++++-- src/imap/command/examined.rs | 16 +-- src/imap/command/selected.rs | 24 ++-- src/imap/flow.rs | 24 ++-- src/imap/mailbox_view.rs | 28 ++-- src/imap/mod.rs | 227 ++++++++++++++++++++---------- src/imap/response.rs | 6 +- src/imap/session.rs | 226 +++++++++-------------------- src/server.rs | 4 +- 11 files changed, 313 insertions(+), 310 deletions(-) diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index 4de5fbd..fbd10e9 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,7 +1,6 @@ use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::{AString, NonEmptyVec}; -use imap_codec::imap_types::response::{Capability, Data}; +use imap_codec::imap_types::core::AString; use imap_codec::imap_types::secret::Secret; use crate::imap::command::anystate; @@ -13,16 +12,16 @@ use crate::mail::user::User; //--- dispatching pub struct AnonymousContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub login_provider: &'a ArcLoginProvider, } -pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), - CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) CommandBody::Login { username, password } => ctx.login(username, password).await, @@ -39,22 +38,11 @@ pub async fn dispatch<'a>(ctx: AnonymousContext<'a>) -> Result<(Response<'a>, fl //--- Command controllers, private impl<'a> AnonymousContext<'a> { - async fn capability(self) -> Result<(Response<'a>, flow::Transition)> { - let capabilities: NonEmptyVec = - (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; - let res = Response::build() - .to_req(self.req) - .message("Server capabilities") - .data(Data::Capability(capabilities)) - .ok()?; - Ok((res, flow::Transition::None)) - } - async fn login( self, username: &AString<'a>, password: &Secret>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let (u, p) = ( std::str::from_utf8(username.as_ref())?, std::str::from_utf8(password.declassify().as_ref())?, diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index ea3bc16..42fe645 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -5,7 +5,7 @@ use imap_codec::imap_types::response::{Capability, Data}; use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { let capabilities: NonEmptyVec = (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; let res = Response::build() @@ -17,7 +17,7 @@ pub(crate) fn capability<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transi Ok((res, flow::Transition::None)) } -pub(crate) fn noop_nothing<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build().tag(tag).message("Noop completed.").ok()?, flow::Transition::None, @@ -41,7 +41,7 @@ pub(crate) fn not_implemented<'a>( )) } -pub(crate) fn wrong_state<'a>(tag: Tag<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .tag(tag) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index c9f9ff7..74ebbfa 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -21,18 +21,18 @@ use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW} use crate::mail::IMF; pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub user: &'a Arc, } pub async fn dispatch<'a>( ctx: AuthenticatedContext<'a>, -) -> Result<(Response<'a>, flow::Transition)> { +) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), - CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) CommandBody::Create { mailbox } => ctx.create(mailbox).await, @@ -68,7 +68,10 @@ pub async fn dispatch<'a>( // --- PRIVATE --- impl<'a> AuthenticatedContext<'a> { - async fn create(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn create( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name = match mailbox { MailboxCodec::Inbox => { return Ok(( @@ -100,7 +103,10 @@ impl<'a> AuthenticatedContext<'a> { } } - async fn delete(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn delete( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; match self.user.delete_mailbox(&name).await { @@ -125,7 +131,7 @@ impl<'a> AuthenticatedContext<'a> { self, from: &MailboxCodec<'a>, to: &MailboxCodec<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(from).try_into()?; let new_name: &str = MailboxName(to).try_into()?; @@ -152,7 +158,7 @@ impl<'a> AuthenticatedContext<'a> { reference: &MailboxCodec<'a>, mailbox_wildcard: &ListMailbox<'a>, is_lsub: bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); let reference: &str = MailboxName(reference).try_into()?; @@ -259,9 +265,9 @@ impl<'a> AuthenticatedContext<'a> { async fn status( self, - mailbox: &MailboxCodec<'a>, + mailbox: &MailboxCodec<'static>, attributes: &[StatusDataItemName], - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(name).await?; let mb = match mb_opt { @@ -316,7 +322,7 @@ impl<'a> AuthenticatedContext<'a> { async fn subscribe( self, mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { @@ -341,7 +347,7 @@ impl<'a> AuthenticatedContext<'a> { async fn unsubscribe( self, mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; if self.user.has_mailbox(&name).await? { @@ -399,7 +405,10 @@ impl<'a> AuthenticatedContext<'a> { * TRACE END --- */ - async fn select(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn select( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -430,7 +439,10 @@ impl<'a> AuthenticatedContext<'a> { )) } - async fn examine(self, mailbox: &MailboxCodec<'a>) -> Result<(Response<'a>, flow::Transition)> { + async fn examine( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; @@ -468,7 +480,7 @@ impl<'a> AuthenticatedContext<'a> { flags: &[Flag<'a>], date: &Option, message: &Literal<'a>, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let append_tag = self.req.tag.clone(); match self.append_internal(mailbox, flags, date, message).await { Ok((_mb, uidvalidity, uid)) => Ok(( diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7f9c39c..eec85cd 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -14,17 +14,17 @@ use crate::imap::response::Response; use crate::mail::user::User; pub struct ExaminedContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } -pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), - CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to the EXAMINE state (specialization of the SELECTED state) // ~3 commands -> close, fetch, search + NOOP @@ -58,7 +58,7 @@ pub async fn dispatch<'a>(ctx: ExaminedContext<'a>) -> Result<(Response<'a>, flo impl<'a> ExaminedContext<'a> { /// CLOSE in examined state is not the same as in selected state /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn close(self) -> Result<(Response<'a>, flow::Transition)> { + async fn close(self) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) @@ -71,9 +71,9 @@ impl<'a> ExaminedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'static>, uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( Response::build() @@ -98,7 +98,7 @@ impl<'a> ExaminedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) @@ -108,7 +108,7 @@ impl<'a> ExaminedContext<'a> { )) } - pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index cd5d221..d5dcd61 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -18,17 +18,19 @@ use crate::imap::response::Response; use crate::mail::user::User; pub struct SelectedContext<'a> { - pub req: &'a Command<'a>, + pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, } -pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response<'a>, flow::Transition)> { +pub async fn dispatch<'a>( + ctx: SelectedContext<'a>, +) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), - CommandBody::Logout => Ok((Response::bye()?, flow::Transition::Logout)), + CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) CommandBody::Close => ctx.close().await, @@ -65,7 +67,7 @@ pub async fn dispatch<'a>(ctx: SelectedContext<'a>) -> Result<(Response<'a>, flo // --- PRIVATE --- impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response<'a>, flow::Transition)> { + async fn close(self) -> Result<(Response<'static>, flow::Transition)> { // We expunge messages, // but we don't send the untagged EXPUNGE responses let tag = self.req.tag.clone(); @@ -79,9 +81,9 @@ impl<'a> SelectedContext<'a> { pub async fn fetch( self, sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'a>, + attributes: &'a MacroOrMessageDataItemNames<'static>, uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { match self.mailbox.fetch(sequence_set, attributes, uid).await { Ok(resp) => Ok(( Response::build() @@ -106,7 +108,7 @@ impl<'a> SelectedContext<'a> { _charset: &Option>, _criteria: &SearchKey<'a>, _uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) @@ -116,7 +118,7 @@ impl<'a> SelectedContext<'a> { )) } - pub async fn noop(self) -> Result<(Response<'a>, flow::Transition)> { + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { self.mailbox.mailbox.force_sync().await?; let updates = self.mailbox.update().await?; @@ -130,7 +132,7 @@ impl<'a> SelectedContext<'a> { )) } - async fn expunge(self) -> Result<(Response<'a>, flow::Transition)> { + async fn expunge(self) -> Result<(Response<'static>, flow::Transition)> { let tag = self.req.tag.clone(); let data = self.mailbox.expunge().await?; @@ -151,7 +153,7 @@ impl<'a> SelectedContext<'a> { response: &StoreResponse, flags: &[Flag<'a>], uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let data = self .mailbox .store(sequence_set, kind, response, flags, uid) @@ -172,7 +174,7 @@ impl<'a> SelectedContext<'a> { sequence_set: &SequenceSet, mailbox: &MailboxCodec<'a>, uid: &bool, - ) -> Result<(Response<'a>, flow::Transition)> { + ) -> Result<(Response<'static>, flow::Transition)> { let name: &str = MailboxName(mailbox).try_into()?; let mb_opt = self.user.open_mailbox(&name).await?; diff --git a/src/imap/flow.rs b/src/imap/flow.rs index eb94bb5..95810c1 100644 --- a/src/imap/flow.rs +++ b/src/imap/flow.rs @@ -37,23 +37,27 @@ pub enum Transition { // See RFC3501 section 3. // https://datatracker.ietf.org/doc/html/rfc3501#page-13 impl State { - pub fn apply(self, tr: Transition) -> Result { - match (self, tr) { - (s, Transition::None) => Ok(s), - (State::NotAuthenticated, Transition::Authenticate(u)) => Ok(State::Authenticated(u)), + pub fn apply(&mut self, tr: Transition) -> Result<(), Error> { + let new_state = match (&self, tr) { + (_s, Transition::None) => return Ok(()), + (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u), ( State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _), Transition::Select(m), - ) => Ok(State::Selected(u, m)), + ) => State::Selected(u.clone(), m), ( State::Authenticated(u) | State::Selected(u, _) | State::Examined(u, _), Transition::Examine(m), - ) => Ok(State::Examined(u, m)), + ) => State::Examined(u.clone(), m), (State::Selected(u, _) | State::Examined(u, _), Transition::Unselect) => { - Ok(State::Authenticated(u)) + State::Authenticated(u.clone()) } - (_, Transition::Logout) => Ok(State::Logout), - _ => Err(Error::ForbiddenTransition), - } + (_, Transition::Logout) => State::Logout, + _ => return Err(Error::ForbiddenTransition), + }; + + *self = new_state; + + Ok(()) } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 2e5444b..fd58de7 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -75,14 +75,26 @@ impl<'a> FetchedMail<'a> { } } -pub struct AttributesProxy<'a> { - attrs: Vec>, +pub struct AttributesProxy { + attrs: Vec>, } -impl<'a> AttributesProxy<'a> { - fn new(attrs: &'a MacroOrMessageDataItemNames<'a>, is_uid_fetch: bool) -> Self { +impl AttributesProxy { + fn new(attrs: &MacroOrMessageDataItemNames<'static>, is_uid_fetch: bool) -> Self { // Expand macros let mut fetch_attrs = match attrs { - MacroOrMessageDataItemNames::Macro(m) => m.expand(), + MacroOrMessageDataItemNames::Macro(m) => { + use imap_codec::imap_types::fetch::Macro; + use MessageDataItemName::*; + match m { + Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope], + Macro::Fast => vec![Flags, InternalDate, Rfc822Size], + Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body], + _ => { + tracing::error!("unimplemented macro"); + vec![] + } + } + } MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(), }; @@ -248,7 +260,7 @@ impl<'a> MailView<'a> { Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) } - fn filter<'b>(&self, ap: &AttributesProxy<'b>) -> Result<(Body<'b>, SeenFlag)> { + fn filter<'b>(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { let mut seen = SeenFlag::DoNothing; let res_attrs = ap .attrs @@ -593,9 +605,9 @@ impl MailboxView { pub async fn fetch<'b>( &self, sequence_set: &SequenceSet, - attributes: &'b MacroOrMessageDataItemNames<'b>, + attributes: &'b MacroOrMessageDataItemNames<'static>, is_uid_fetch: &bool, - ) -> Result>> { + ) -> Result>> { let ap = AttributesProxy::new(attributes, *is_uid_fetch); // Prepare data diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 589231b..31eeaa8 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -4,104 +4,183 @@ mod mailbox_view; mod response; mod session; -use std::task::{Context, Poll}; +use std::net::SocketAddr; use anyhow::Result; -//use boitalettres::errors::Error as BalError; -//use boitalettres::proto::{Request, Response}; -//use boitalettres::server::accept::addr::AddrIncoming; -//use boitalettres::server::accept::addr::AddrStream; -//use boitalettres::server::Server as ImapServer; -use futures::future::BoxFuture; -use futures::future::FutureExt; +use futures::stream::{FuturesUnordered, StreamExt}; + +use tokio::net::TcpListener; use tokio::sync::watch; +use imap_codec::imap_types::response::Greeting; +use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; +use imap_flow::stream::AnyStream; + use crate::config::ImapConfig; use crate::login::ArcLoginProvider; /// Server is a thin wrapper to register our Services in BàL -pub struct Server {} - -pub async fn new(config: ImapConfig, login: ArcLoginProvider) -> Result { - unimplemented!(); - /* let incoming = AddrIncoming::new(config.bind_addr).await?; - tracing::info!("IMAP activated, will listen on {:#}", incoming.local_addr); - - let imap = ImapServer::new(incoming).serve(Instance::new(login.clone())); - Ok(Server(imap))*/ -} - -impl Server { - pub async fn run(self, mut must_exit: watch::Receiver) -> Result<()> { - tracing::info!("IMAP started!"); - unimplemented!(); - /*tokio::select! { - s = self.0 => s?, - _ = must_exit.changed() => tracing::info!("Stopped IMAP server"), - } - - Ok(())*/ - } -} - -//--- -/* -/// Instance is the main Tokio Tower service that we register in BàL. -/// It receives new connection demands and spawn a dedicated service. -struct Instance { +pub struct Server { + bind_addr: SocketAddr, login_provider: ArcLoginProvider, } -impl Instance { - pub fn new(login_provider: ArcLoginProvider) -> Self { - Self { login_provider } +struct ClientContext { + stream: AnyStream, + addr: SocketAddr, + login_provider: ArcLoginProvider, + must_exit: watch::Receiver, +} + +pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server { + Server { + bind_addr: config.bind_addr, + login_provider: login, } } -impl<'a> Service<&'a AddrStream> for Instance { - type Response = Connection; - type Error = anyhow::Error; - type Future = BoxFuture<'static, Result>; +impl Server { + pub async fn run(self: Self, mut must_exit: watch::Receiver) -> Result<()> { + let tcp = TcpListener::bind(self.bind_addr).await?; + tracing::info!("IMAP server listening on {:#}", self.bind_addr); - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + let mut connections = FuturesUnordered::new(); - fn call(&mut self, addr: &'a AddrStream) -> Self::Future { - tracing::info!(remote_addr = %addr.remote_addr, local_addr = %addr.local_addr, "accept"); - let lp = self.login_provider.clone(); - async { Ok(Connection::new(lp)) }.boxed() + while !*must_exit.borrow() { + let wait_conn_finished = async { + if connections.is_empty() { + futures::future::pending().await + } else { + connections.next().await + } + }; + let (socket, remote_addr) = tokio::select! { + a = tcp.accept() => a?, + _ = wait_conn_finished => continue, + _ = must_exit.changed() => continue, + }; + tracing::info!("IMAP: accepted connection from {}", remote_addr); + + let client = ClientContext { + stream: AnyStream::new(socket), + addr: remote_addr.clone(), + login_provider: self.login_provider.clone(), + must_exit: must_exit.clone(), + }; + let conn = tokio::spawn(client_wrapper(client)); + connections.push(conn); + } + drop(tcp); + + tracing::info!("IMAP server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} + + Ok(()) } } -//--- - -/// Connection is the per-connection Tokio Tower service we register in BàL. -/// It handles a single TCP connection, and thus has a business logic. -struct Connection { - session: session::Manager, -} - -impl Connection { - pub fn new(login_provider: ArcLoginProvider) -> Self { - Self { - session: session::Manager::new(login_provider), +async fn client_wrapper(ctx: ClientContext) { + let addr = ctx.addr.clone(); + match client(ctx).await { + Ok(()) => { + tracing::info!("closing successful session for {:?}", addr); + } + Err(e) => { + tracing::error!("closing errored session for {:?}: {}", addr, e); } } } -impl Service for Connection { - type Response = Response; - type Error = BalError; - type Future = BoxFuture<'static, Result>; +async fn client(mut ctx: ClientContext) -> Result<()> { + // Send greeting + let (mut server, _) = ServerFlow::send_greeting( + ctx.stream, + ServerFlowOptions::default(), + Greeting::ok(None, "Aerogramme").unwrap(), + ) + .await?; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) + use crate::imap::response::{Body, Response as MyResponse}; + use crate::imap::session::Instance; + use imap_codec::imap_types::command::Command; + use imap_codec::imap_types::response::{Response, Status}; + + use tokio::sync::mpsc; + let (cmd_tx, mut cmd_rx) = mpsc::channel::>(10); + let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::>(); + + let bckgrnd = tokio::spawn(async move { + let mut session = Instance::new(ctx.login_provider); + loop { + let cmd = match cmd_rx.recv().await { + None => break, + Some(cmd_recv) => cmd_recv, + }; + + let maybe_response = session.command(cmd).await; + + match resp_tx.send(maybe_response) { + Err(_) => break, + Ok(_) => (), + }; + } + tracing::info!("runner is quitting"); + }); + + // Main loop + loop { + tokio::select! { + // Managing imap_flow stuff + srv_evt = server.progress() => match srv_evt? { + ServerFlowEvent::ResponseSent { handle: _handle, response } => { + match response { + Response::Status(Status::Bye(_)) => break, + _ => tracing::trace!("sent to {} content {:?}", ctx.addr, response), + } + }, + ServerFlowEvent::CommandReceived { command } => { + match cmd_tx.try_send(command) { + Ok(_) => (), + Err(mpsc::error::TrySendError::Full(_)) => { + server.enqueue_status(Status::bye(None, "Too fast").unwrap()); + tracing::error!("client {:?} is sending commands too fast, closing.", ctx.addr); + } + _ => { + server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); + tracing::error!("session task exited for {:?}, quitting", ctx.addr); + } + } + }, + }, + + // Managing response generated by Aerogramme + maybe_msg = resp_rx.recv() => { + let response = match maybe_msg { + None => { + server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); + tracing::error!("session task exited for {:?}, quitting", ctx.addr); + continue + }, + Some(r) => r, + }; + + for body_elem in response.body.into_iter() { + let _handle = match body_elem { + Body::Data(d) => server.enqueue_data(d), + Body::Status(s) => server.enqueue_status(s), + }; + } + server.enqueue_status(response.completion); + }, + + // When receiving a CTRL+C + _ = ctx.must_exit.changed() => { + server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); + }, + }; } - fn call(&mut self, req: Request) -> Self::Future { - tracing::debug!("Got request: {:#?}", req.command); - self.session.process(req) - } + drop(cmd_tx); + bckgrnd.await?; + Ok(()) } -*/ diff --git a/src/imap/response.rs b/src/imap/response.rs index 012c8ed..d20e58e 100644 --- a/src/imap/response.rs +++ b/src/imap/response.rs @@ -47,11 +47,13 @@ impl<'a> ResponseBuilder<'a> { self } + #[allow(dead_code)] pub fn info(mut self, status: Status<'a>) -> Self { self.body.push(Body::Status(status)); self } + #[allow(dead_code)] pub fn many_info(mut self, status: Vec>) -> Self { for d in status.into_iter() { self = self.info(d); @@ -87,8 +89,8 @@ impl<'a> ResponseBuilder<'a> { } pub struct Response<'a> { - body: Vec>, - completion: Status<'a>, + pub body: Vec>, + pub completion: Status<'a>, } impl<'a> Response<'a> { diff --git a/src/imap/session.rs b/src/imap/session.rs index e2af18b..5c67f8e 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,182 +1,86 @@ -use anyhow::Error; -//use boitalettres::errors::Error as BalError; -//use boitalettres::proto::{Request, Response}; -use futures::future::BoxFuture; -use futures::future::FutureExt; - -use tokio::sync::mpsc::error::TrySendError; -use tokio::sync::{mpsc, oneshot}; - use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; +use crate::imap::response::Response; use crate::login::ArcLoginProvider; - -/* -/* This constant configures backpressure in the system, - * or more specifically, how many pipelined messages are allowed - * before refusing them - */ -const MAX_PIPELINED_COMMANDS: usize = 10; - -struct Message { - req: Request, - tx: oneshot::Sender>, -} +use imap_codec::imap_types::command::Command; //----- - -pub struct Manager { - tx: mpsc::Sender, -} - -impl Manager { - pub fn new(login_provider: ArcLoginProvider) -> Self { - let (tx, rx) = mpsc::channel(MAX_PIPELINED_COMMANDS); - tokio::spawn(async move { - let instance = Instance::new(login_provider, rx); - instance.start().await; - }); - Self { tx } - } - - pub fn process(&self, req: Request) -> BoxFuture<'static, Result> { - let (tx, rx) = oneshot::channel(); - let msg = Message { req, tx }; - - // We use try_send on a bounded channel to protect the daemons from DoS. - // Pipelining requests in IMAP are a special case: they should not occure often - // and in a limited number (like 3 requests). Someone filling the channel - // will probably be malicious so we "rate limit" them. - match self.tx.try_send(msg) { - Ok(()) => (), - Err(TrySendError::Full(_)) => { - return async { Response::bad("Too fast! Send less pipelined requests.") }.boxed() - } - Err(TrySendError::Closed(_)) => { - return async { Err(BalError::Text("Terminated session".to_string())) }.boxed() - } - }; - - // @FIXME add a timeout, handle a session that fails. - async { - match rx.await { - Ok(r) => r, - Err(e) => { - tracing::warn!("Got error {:#?}", e); - Response::bad("No response from the session handler") - } - } - } - .boxed() - } -} -*/ -//----- -/* pub struct Instance { - rx: mpsc::Receiver, - pub login_provider: ArcLoginProvider, pub state: flow::State, } impl Instance { - fn new(login_provider: ArcLoginProvider, rx: mpsc::Receiver) -> Self { + pub fn new(login_provider: ArcLoginProvider) -> Self { Self { login_provider, - rx, state: flow::State::NotAuthenticated, } } - //@FIXME add a function that compute the runner's name from its local info - // to ease debug - // fn name(&self) -> String { } - - async fn start(mut self) { - //@FIXME add more info about the runner - tracing::debug!("starting runner"); - - while let Some(msg) = self.rx.recv().await { - // Command behavior is modulated by the state. - // To prevent state error, we handle the same command in separate code paths. - let ctrl = match &mut self.state { - flow::State::NotAuthenticated => { - let ctx = anonymous::AnonymousContext { - req: &msg.req, - login_provider: Some(&self.login_provider), - }; - anonymous::dispatch(ctx).await - } - flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &msg.req, - user, - }; - authenticated::dispatch(ctx).await - } - flow::State::Selected(ref user, ref mut mailbox) => { - let ctx = selected::SelectedContext { - req: &msg.req, - user, - mailbox, - }; - selected::dispatch(ctx).await - } - flow::State::Examined(ref user, ref mut mailbox) => { - let ctx = examined::ExaminedContext { - req: &msg.req, - user, - mailbox, - }; - examined::dispatch(ctx).await - } - flow::State::Logout => { - Response::bad("No commands are allowed in the LOGOUT state.") - .map(|r| (r, flow::Transition::None)) - .map_err(Error::msg) - } - }; - - // Process result - let res = match ctrl { - Ok((res, tr)) => { - //@FIXME remove unwrap - self.state = match self.state.apply(tr) { - Ok(new_state) => new_state, - Err(e) => { - tracing::error!("Invalid transition: {}, exiting", e); - break; - } - }; - - //@FIXME enrich here the command with some global status - - Ok(res) - } - // Cast from anyhow::Error to Bal::Error - // @FIXME proper error handling would be great - Err(e) => match e.downcast::() { - Ok(be) => Err(be), - Err(e) => { - tracing::warn!(error=%e, "internal.error"); - Response::bad("Internal error") - } - }, - }; - - //@FIXME I think we should quit this thread on error and having our manager watch it, - // and then abort the session as it is corrupted. - msg.tx.send(res).unwrap_or_else(|e| { - tracing::warn!("failed to send imap response to manager: {:#?}", e) - }); - - if let flow::State::Logout = &self.state { - break; + pub async fn command(&mut self, cmd: Command<'static>) -> Response<'static> { + // Command behavior is modulated by the state. + // To prevent state error, we handle the same command in separate code paths. + let (resp, tr) = match &mut self.state { + flow::State::NotAuthenticated => { + let ctx = anonymous::AnonymousContext { + req: &cmd, + login_provider: &self.login_provider, + }; + anonymous::dispatch(ctx).await } + flow::State::Authenticated(ref user) => { + let ctx = authenticated::AuthenticatedContext { req: &cmd, user }; + authenticated::dispatch(ctx).await + } + flow::State::Selected(ref user, ref mut mailbox) => { + let ctx = selected::SelectedContext { + req: &cmd, + user, + mailbox, + }; + selected::dispatch(ctx).await + } + flow::State::Examined(ref user, ref mut mailbox) => { + let ctx = examined::ExaminedContext { + req: &cmd, + user, + mailbox, + }; + examined::dispatch(ctx).await + } + flow::State::Logout => Response::build() + .tag(cmd.tag.clone()) + .message("No commands are allowed in the LOGOUT state.") + .bad() + .map(|r| (r, flow::Transition::None)), + } + .unwrap_or_else(|err| { + tracing::error!("Command error {:?} occured while processing {:?}", err, cmd); + ( + Response::build() + .to_req(&cmd) + .message("Internal error while processing command") + .bad() + .unwrap(), + flow::Transition::None, + ) + }); + + if let Err(e) = self.state.apply(tr) { + tracing::error!( + "Transition error {:?} occured while processing on command {:?}", + e, + cmd + ); + return Response::build() + .to_req(&cmd) + .message( + "Internal error, processing command triggered an illegal IMAP state transition", + ) + .bad() + .unwrap(); } - //@FIXME add more info about the runner - tracing::debug!("exiting runner"); + resp } } -*/ diff --git a/src/server.rs b/src/server.rs index 8bfde98..bd2fd5d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -25,7 +25,7 @@ impl Server { let login = Arc::new(StaticLoginProvider::new(config.users).await?); let lmtp_server = None; - let imap_server = Some(imap::new(config.imap, login.clone()).await?); + let imap_server = Some(imap::new(config.imap, login.clone())); Ok(Self { lmtp_server, imap_server, @@ -42,7 +42,7 @@ impl Server { }; let lmtp_server = Some(LmtpServer::new(config.lmtp, login.clone())); - let imap_server = Some(imap::new(config.imap, login.clone()).await?); + let imap_server = Some(imap::new(config.imap, login.clone())); Ok(Self { lmtp_server, From b66b9f75fe0c078dfd34dd45d5ce80786aba8c2c Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 22:09:45 +0100 Subject: [PATCH 15/18] fixed aerogramme tests --- src/imap/mailbox_view.rs | 50 ++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index fd58de7..7434512 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1309,18 +1309,22 @@ mod tests { use super::*; use crate::cryptoblob; use crate::mail::unique_ident; - use imap_codec::codec::Encode; + use imap_codec::encode::Encoder; use imap_codec::imap_types::fetch::Section; + use imap_codec::imap_types::response::Response; + use imap_codec::ResponseCodec; use std::fs; #[test] fn mailview_body_ext() -> Result<()> { let ap = AttributesProxy::new( - &MacroOrMessageDataItemNames::FetchAttributes(vec![MessageDataItemName::BodyExt { - section: Some(Section::Header(None)), - partial: None, - peek: false, - }]), + &MacroOrMessageDataItemNames::MessageDataItemNames(vec![ + MessageDataItemName::BodyExt { + section: Some(Section::Header(None)), + partial: None, + peek: false, + }, + ]), false, ); @@ -1340,13 +1344,13 @@ mod tests { let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; let content = FetchedMail::new_from_message(eml_codec::parse_message(rfc822)?.1); - let mut mv = MailView { + let mv = MailView { ids: &ids, content, meta: &meta, flags: &flags, }; - let res_body = mv.filter(&ap)?; + let (res_body, _seen) = mv.filter(&ap)?; let fattr = match res_body { Body::Data(Data::Fetch { @@ -1356,10 +1360,10 @@ mod tests { _ => Err(anyhow!("Not a fetch body")), }?; - assert_eq!(fattr.len(), 1); + assert_eq!(fattr.as_ref().len(), 1); - let (sec, _orig, _data) = match &fattr[0] { - MessageDataItemName::BodyExt { + let (sec, _orig, _data) = match &fattr.as_ref()[0] { + MessageDataItem::BodyExt { section, origin, data, @@ -1408,22 +1412,24 @@ mod tests { for pref in prefixes.iter() { println!("{}", pref); let txt = fs::read(format!("{}.eml", pref))?; - let exp = fs::read(format!("{}.dovecot.body", pref))?; + let oracle = fs::read(format!("{}.dovecot.body", pref))?; let message = eml_codec::parse_message(&txt).unwrap().1; - let mut resp = Vec::new(); - MessageDataItemName::Body(build_imap_email_struct(&message.child)?) - .encode(&mut resp) - .unwrap(); + let test_repr = Response::Data(Data::Fetch { + seq: NonZeroU32::new(1).unwrap(), + items: NonEmptyVec::from(MessageDataItem::Body(build_imap_email_struct( + &message.child, + )?)), + }); + let test_bytes = ResponseCodec::new().encode(&test_repr).dump(); + let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase(); - let resp_str = String::from_utf8_lossy(&resp).to_lowercase(); + let oracle_str = + format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase(); - let exp_no_parenthesis = &exp[1..exp.len() - 1]; - let exp_str = String::from_utf8_lossy(exp_no_parenthesis).to_lowercase(); - - println!("aerogramme: {}\n\ndovecot: {}\n\n", resp_str, exp_str); + println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str); //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); - assert_eq!(resp_str, exp_str); + assert_eq!(test_str, oracle_str); } Ok(()) From 0cc13f891cdcdc474416cdb63d48245a1820af10 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 22:32:02 +0100 Subject: [PATCH 16/18] migration to imap-flow seems done! --- src/imap/command/authenticated.rs | 1 + src/imap/command/examined.rs | 12 +++++++++--- src/imap/command/selected.rs | 12 +++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 74ebbfa..1bb4c6d 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -432,6 +432,7 @@ impl<'a> AuthenticatedContext<'a> { Ok(( Response::build() .message("Select completed") + .to_req(self.req) .code(Code::ReadWrite) .set_body(data) .ok()?, diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index eec85cd..7de94f4 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,7 +7,7 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::command::anystate; +use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; @@ -48,8 +48,14 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl flow::Transition::None, )), - // The command does not belong to this state - _ => anystate::wrong_state(ctx.req.tag.clone()), + // In examined mode, we fallback to authenticated when needed + _ => { + authenticated::dispatch(authenticated::AuthenticatedContext { + req: ctx.req, + user: ctx.user, + }) + .await + } } } diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index d5dcd61..220a952 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,7 +10,7 @@ use imap_codec::imap_types::response::{Code, CodeOther}; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::command::{anystate, MailboxName}; +use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; @@ -59,8 +59,14 @@ pub async fn dispatch<'a>( uid, } => ctx.copy(sequence_set, mailbox, uid).await, - // The command does not belong to this state - _ => anystate::wrong_state(ctx.req.tag.clone()), + // In selected mode, we fallback to authenticated when needed + _ => { + authenticated::dispatch(authenticated::AuthenticatedContext { + req: ctx.req, + user: ctx.user, + }) + .await + } } } From f480ff0d3150f5cadb35ad40b270693b838ff486 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 23:42:47 +0100 Subject: [PATCH 17/18] tested append --- tests/imap_features.rs | 61 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 3479292..79d740f 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -5,7 +5,7 @@ use std::process::Command; use std::{thread, time}; static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); -static EMAIL: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r +static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r From: Bob Robert \r To: Alice Malice \r CC: =?ISO-8859-1?Q?Andr=E9?= Pirard \r @@ -49,6 +49,13 @@ OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r --b1_e376dc71bafc953c0b0fdeb9983a9956--\r "; +static EMAIL2: &[u8] = b"From: alice@example.com\r +To: alice@example.tld\r +Subject: Test\r +\r +Hello world!\r +"; + fn main() { let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) .arg("--dev") @@ -95,14 +102,14 @@ fn generic_test(imap_socket: &mut TcpStream, lmtp_socket: &mut TcpStream) -> Res // UNSUBSCRIBE IS NOT IMPLEMENTED YET //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; select_inbox(imap_socket).context("select inbox")?; - // CHECK IS NOT IMPLEMENTED YET - //check(...) + check(imap_socket).context("check must run")?; status_mailbox(imap_socket).context("status of archive from inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; + lmtp_deliver_email(lmtp_socket, EMAIL1).context("mail delivered successfully")?; noop_exists(imap_socket).context("noop loop must detect a new email")?; - fetch_rfc822(imap_socket, EMAIL).context("fetch rfc822 message")?; + fetch_rfc822(imap_socket, EMAIL1).context("fetch rfc822 message")?; copy_email(imap_socket).context("copy message to the archive mailbox")?; + append_email(imap_socket, EMAIL2).context("insert email in INBOX")?; // SEARCH IS NOT IMPLEMENTED YET //search(imap_socket).expect("search should return something"); add_flags_email(imap_socket).context("should add delete and important flags to the email")?; @@ -186,6 +193,15 @@ fn select_inbox(imap: &mut TcpStream) -> Result<()> { Ok(()) } +fn check(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + imap.write(&b"21 check\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?; + + Ok(()) +} + fn status_mailbox(imap: &mut TcpStream) -> Result<()> { imap.write(&b"25 STATUS archive (UIDNEXT MESSAGES)\r\n"[..])?; let mut buffer: [u8; 6000] = [0; 6000]; @@ -268,9 +284,41 @@ fn copy_email(imap: &mut TcpStream) -> Result<()> { Ok(()) } +fn append_email(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + assert_ne!(ref_mail.len(), 0); + let append_cmd = format!("47 append inbox (\\Seen) {{{}}}\r\n", ref_mail.len()); + println!("append cmd: {}", append_cmd); + imap.write(append_cmd.as_bytes())?; + + // wait for continuation + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(read[0], b'+'); + + // write our stuff + imap.write(ref_mail)?; + imap.write(&b"\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"47 OK"[..]); + + // noop to force a sync + imap.write(&b"48 NOOP\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"48 OK NOOP"[..]))?; + + // check it is stored successfully + imap.write(&b"49 fetch 2 rfc822.size\r\n"[..])?; + println!("sent fetch size"); + let read = read_lines(imap, &mut buffer, Some(&b"49 OK"[..]))?; + let expected = format!("* 2 FETCH (RFC822.SIZE {})", ref_mail.len()); + let expbytes = expected.as_bytes(); + assert_eq!(&read[..expbytes.len()], expbytes); + + Ok(()) +} + fn add_flags_email(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"50 store 1 +FLAGS (\\Deleted \\Important)\r\n"[..])?; let mut buffer: [u8; 1500] = [0; 1500]; + imap.write(&b"50 store 1 +FLAGS (\\Deleted \\Important)\r\n"[..])?; let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; Ok(()) @@ -333,6 +381,7 @@ fn read_lines<'a, F: Read>( let mut nbytes = 0; loop { nbytes += reader.read(&mut buffer[nbytes..])?; + println!("partial read: {}", std::str::from_utf8(&buffer[..nbytes])?); let pre_condition = match stop_marker { None => true, Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark), From c9a33c080d39d4a2b269e3c8f166a708b6606da5 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 2 Jan 2024 23:43:58 +0100 Subject: [PATCH 18/18] clean tests --- tests/imap_features.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/imap_features.rs b/tests/imap_features.rs index 79d740f..9e8d587 100644 --- a/tests/imap_features.rs +++ b/tests/imap_features.rs @@ -307,7 +307,6 @@ fn append_email(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { // check it is stored successfully imap.write(&b"49 fetch 2 rfc822.size\r\n"[..])?; - println!("sent fetch size"); let read = read_lines(imap, &mut buffer, Some(&b"49 OK"[..]))?; let expected = format!("* 2 FETCH (RFC822.SIZE {})", ref_mail.len()); let expbytes = expected.as_bytes(); @@ -381,7 +380,7 @@ fn read_lines<'a, F: Read>( let mut nbytes = 0; loop { nbytes += reader.read(&mut buffer[nbytes..])?; - println!("partial read: {}", std::str::from_utf8(&buffer[..nbytes])?); + //println!("partial read: {}", std::str::from_utf8(&buffer[..nbytes])?); let pre_condition = match stop_marker { None => true, Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark),