diff --git a/Cargo.lock b/Cargo.lock index 1b9cc078..23322db1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -176,7 +182,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -198,7 +204,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -209,7 +215,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -303,9 +309,9 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -453,9 +459,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" @@ -531,13 +537,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", "time 0.1.45", @@ -902,7 +908,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -1276,7 +1282,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -1718,9 +1724,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -1782,7 +1788,7 @@ dependencies = [ "aes-gcm", "aes-gcm-siv", "async-stream", - "base64 0.21.1", + "base64 0.21.2", "bincode", "chrono", "directory", @@ -2003,12 +2009,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "lru-cache" @@ -2031,8 +2034,8 @@ dependencies = [ [[package]] name = "mail-auth" -version = "0.3.1" -source = "git+https://github.com/stalwartlabs/mail-auth#e589e5daf8ad12be96c98fa11193fd73afe450f4" +version = "0.3.2" +source = "git+https://github.com/stalwartlabs/mail-auth#0c93f7b18a6370bd3c86671e4d121c0f0b2f0b23" dependencies = [ "ahash 0.8.3", "flate2", @@ -2051,8 +2054,8 @@ dependencies = [ [[package]] name = "mail-builder" -version = "0.2.5" -source = "git+https://github.com/stalwartlabs/mail-builder#589dc325508c905c6248d13596a82cd1e89e8315" +version = "0.3.0" +source = "git+https://github.com/stalwartlabs/mail-builder#e771643f2f4ff8ea86f91f7985b1efa6ffa8aa85" dependencies = [ "gethostname", ] @@ -2068,8 +2071,8 @@ dependencies = [ [[package]] name = "mail-send" -version = "0.3.3" -source = "git+https://github.com/stalwartlabs/mail-send#f4d58ebf16e0ea6083cfd2d482114ad8ae201c35" +version = "0.4.0" +source = "git+https://github.com/stalwartlabs/mail-send#d5ac9b328308fd95709cb8ee1c3ce37716e210ef" dependencies = [ "base64 0.20.0", "gethostname", @@ -2081,7 +2084,7 @@ dependencies = [ "smtp-proto", "tokio", "tokio-rustls 0.24.0", - "webpki-roots 0.23.0", + "webpki-roots 0.23.1", ] [[package]] @@ -2224,14 +2227,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2354,9 +2356,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "opaque-debug" @@ -2366,9 +2368,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -2387,7 +2389,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -2398,9 +2400,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" dependencies = [ "cc", "libc", @@ -2720,7 +2722,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -2803,9 +2805,9 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8d3875361e28f7753baefef104386e7aa47642c93023356d97fdef4003bfb5" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" dependencies = [ "elliptic-curve", ] @@ -2822,9 +2824,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -2906,18 +2908,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.27.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -3005,13 +3007,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -3031,9 +3033,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "reqwest" @@ -3041,7 +3043,7 @@ version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.1", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -3254,7 +3256,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.1", + "base64 0.21.2", ] [[package]] @@ -3388,7 +3390,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -3436,7 +3438,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -3504,8 +3506,8 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "sieve-rs" -version = "0.3.0" -source = "git+https://github.com/stalwartlabs/sieve#83f1f9cead81625a8f512434ef7b701588754ea2" +version = "0.3.1" +source = "git+https://github.com/stalwartlabs/sieve#0ab2dc8cd41ee5dadcc3ab5e932b9b92abc5e067" dependencies = [ "ahash 0.8.3", "bincode", @@ -3592,7 +3594,7 @@ dependencies = [ "tokio-rustls 0.24.0", "tracing", "utils", - "webpki-roots 0.23.0", + "webpki-roots 0.23.1", "x509-parser 0.15.0", ] @@ -3711,7 +3713,7 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.23.0", + "webpki-roots 0.23.1", ] [[package]] @@ -3760,7 +3762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c430536df19e8b5b048a9ae19b266aba77f9f3e2255b7195f465d678cb2d0a" dependencies = [ "atoi", - "base64 0.21.1", + "base64 0.21.2", "bitflags 1.3.2", "byteorder", "bytes", @@ -3802,7 +3804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "210e0a1523b6d46ca73db1c5197a233a8e14787596910ce88ff5d47a00da0241" dependencies = [ "atoi", - "base64 0.21.1", + "base64 0.21.2", "bitflags 1.3.2", "byteorder", "crc", @@ -3930,9 +3932,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -3985,7 +3987,7 @@ version = "0.1.0" dependencies = [ "ahash 0.8.3", "async-trait", - "base64 0.21.1", + "base64 0.21.2", "bytes", "chrono", "csv", @@ -3996,6 +3998,7 @@ dependencies = [ "futures", "http-body-util", "hyper 1.0.0-rc.3", + "jmap", "jmap-client", "jmap_proto", "mail-auth", @@ -4044,7 +4047,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -4122,9 +4125,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.1" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", @@ -4157,7 +4160,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -4204,7 +4207,7 @@ dependencies = [ "tokio", "tokio-rustls 0.24.0", "tungstenite", - "webpki-roots 0.23.0", + "webpki-roots 0.23.1", ] [[package]] @@ -4330,7 +4333,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -4490,9 +4493,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -4643,7 +4646,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", "wasm-bindgen-shared", ] @@ -4677,7 +4680,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4732,9 +4735,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ "rustls-webpki", ] @@ -5005,9 +5008,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.11" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1690519550bfa95525229b9ca2350c63043a4857b3b0013811b2ccf4a2420b01" +checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" [[package]] name = "xxhash-rust" diff --git a/crates/directory/src/config.rs b/crates/directory/src/config.rs index 58129aad..7881ead4 100644 --- a/crates/directory/src/config.rs +++ b/crates/directory/src/config.rs @@ -26,23 +26,37 @@ impl ConfigDirectory for Config { lookups: AHashMap::new(), }; for id in self.sub_keys("directory") { - // Parse domains list - let domains = self.parse_lookup_list(("directory", id, "lookup.domains"))?; - // Parse directory - let protocol = self.value_require(("directory", id, "protocol"))?; + let protocol = self.value_require(("directory", id, "type"))?; + let prefix = ("directory", id); let directory = match protocol { - "ldap" => LdapDirectory::from_config(self, ("directory", id), domains)?, - "sql" => SqlDirectory::from_config(self, ("directory", id), domains)?, - "imap" => ImapDirectory::from_config(self, ("directory", id), domains)?, - "smtp" => SmtpDirectory::from_config(self, ("directory", id), domains, false)?, - "lmtp" => SmtpDirectory::from_config(self, ("directory", id), domains, true)?, - "memory" => MemoryDirectory::from_config(self, ("directory", id))?, + "ldap" => LdapDirectory::from_config(self, prefix)?, + "sql" => SqlDirectory::from_config(self, prefix)?, + "imap" => ImapDirectory::from_config(self, prefix)?, + "smtp" => SmtpDirectory::from_config(self, prefix, false)?, + "lmtp" => SmtpDirectory::from_config(self, prefix, true)?, + "memory" => MemoryDirectory::from_config(self, prefix)?, unknown => { return Err(format!("Unknown directory type: {unknown:?}")); } }; + // Add queries/filters as lookups + if ["sql", "ldap"].contains(&protocol) { + let name = if protocol == "sql" { "query" } else { "filter" }; + for lookup_id in self.sub_keys(("directory", id, name)) { + config.lookups.insert( + format!("{id}/{lookup_id}"), + Arc::new(Lookup::Directory { + directory: directory.clone(), + query: self + .value_require(("directory", id, name, lookup_id))? + .to_string(), + }), + ); + } + } + // Parse lookups let is_remote = protocol != "memory"; for lookup_id in self.sub_keys(("directory", id, "lookup")) { diff --git a/crates/directory/src/imap/config.rs b/crates/directory/src/imap/config.rs index 563e9b48..2f6923ec 100644 --- a/crates/directory/src/imap/config.rs +++ b/crates/directory/src/imap/config.rs @@ -1,10 +1,14 @@ use std::sync::Arc; -use ahash::AHashSet; use mail_send::smtp::tls::build_tls_connector; use utils::config::{utils::AsKey, Config}; -use crate::{cache::CachedDirectory, config::build_pool, imap::ImapConnectionManager, Directory}; +use crate::{ + cache::CachedDirectory, + config::{build_pool, ConfigDirectory}, + imap::ImapConnectionManager, + Directory, +}; use super::ImapDirectory; @@ -12,7 +16,6 @@ impl ImapDirectory { pub fn from_config( config: &Config, prefix: impl AsKey, - domains: AHashSet, ) -> utils::config::Result> { let prefix = prefix.as_key(); let address = config.value_require((&prefix, "address"))?; @@ -36,7 +39,7 @@ impl ImapDirectory { &prefix, ImapDirectory { pool: build_pool(config, &prefix, manager)?, - domains, + domains: config.parse_lookup_list((&prefix, "lookup.domains"))?, }, ) } diff --git a/crates/directory/src/imap/lookup.rs b/crates/directory/src/imap/lookup.rs index f3ae3493..e70d393d 100644 --- a/crates/directory/src/imap/lookup.rs +++ b/crates/directory/src/imap/lookup.rs @@ -32,7 +32,7 @@ impl Directory for ImapDirectory { tracing::warn!( context = "remote", event = "error", - protocol = "imap", + protocol = "ldap", "IMAP server does not offer any supported auth mechanisms.", ); return Ok(None); diff --git a/crates/directory/src/ldap/config.rs b/crates/directory/src/ldap/config.rs index db07e0da..cf77fbf1 100644 --- a/crates/directory/src/ldap/config.rs +++ b/crates/directory/src/ldap/config.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use ahash::AHashSet; use ldap3::LdapConnSettings; use utils::config::{utils::AsKey, Config}; @@ -12,7 +11,6 @@ impl LdapDirectory { pub fn from_config( config: &Config, prefix: impl AsKey, - domains: AHashSet, ) -> utils::config::Result> { let prefix = prefix.as_key(); let bind_dn = if let Some(dn) = config.value((&prefix, "bind.dn")) { @@ -107,7 +105,6 @@ impl LdapDirectory { LdapDirectory { mappings, pool: build_pool(config, &prefix, manager)?, - domains, }, ) } diff --git a/crates/directory/src/ldap/lookup.rs b/crates/directory/src/ldap/lookup.rs index aa7f4085..e37a3514 100644 --- a/crates/directory/src/ldap/lookup.rs +++ b/crates/directory/src/ldap/lookup.rs @@ -21,7 +21,7 @@ impl Directory for LdapDirectory { .await { Ok(Some(principal)) => { - if principal.verify_secret(secret) { + if principal.verify_secret(secret).await { Ok(Some(principal)) } else { Ok(None) @@ -253,9 +253,6 @@ impl Directory for LdapDirectory { } async fn is_local_domain(&self, domain: &str) -> crate::Result { - if self.domains.contains(domain) { - return Ok(true); - } self.pool .get() .await? diff --git a/crates/directory/src/ldap/mod.rs b/crates/directory/src/ldap/mod.rs index a5b891b2..873e286e 100644 --- a/crates/directory/src/ldap/mod.rs +++ b/crates/directory/src/ldap/mod.rs @@ -1,4 +1,3 @@ -use ahash::AHashSet; use bb8::Pool; use ldap3::{ldap_escape, LdapConnSettings}; @@ -9,7 +8,6 @@ pub mod pool; pub struct LdapDirectory { pool: Pool, mappings: LdapMappings, - domains: AHashSet, } #[derive(Debug, Default)] diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 763249f9..41272fd9 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -102,6 +102,28 @@ impl PartialEq for Lookup { impl Eq for Lookup {} +impl Principal { + pub fn id(&self) -> u32 { + self.id + } + + pub fn has_id(&self) -> bool { + self.id != u32::MAX + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn has_name(&self) -> bool { + !self.name.is_empty() + } + + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } +} + impl Debug for dyn Directory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Directory") @@ -189,7 +211,7 @@ impl From for DirectoryError { tracing::warn!( context = "directory", event = "error", - protocol = "imap", + protocol = "ldap", reason = %error, "IMAP directory error" ); diff --git a/crates/directory/src/memory/config.rs b/crates/directory/src/memory/config.rs index b6deb626..2dea4299 100644 --- a/crates/directory/src/memory/config.rs +++ b/crates/directory/src/memory/config.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use utils::config::{utils::AsKey, Config}; -use crate::{Directory, Principal, Type}; +use crate::{config::ConfigDirectory, Directory, Principal, Type}; use super::{EmailType, MemoryDirectory}; @@ -95,6 +95,8 @@ impl MemoryDirectory { }); } + directory.domains = config.parse_lookup_list((&prefix, "lookup.domains"))?; + Ok(Arc::new(directory)) } } diff --git a/crates/directory/src/memory/lookup.rs b/crates/directory/src/memory/lookup.rs index 65b16572..63ed8e3f 100644 --- a/crates/directory/src/memory/lookup.rs +++ b/crates/directory/src/memory/lookup.rs @@ -20,7 +20,7 @@ impl Directory for MemoryDirectory { .get(username) .and_then(|id| self.principals.get(*id as usize)) { - Some(principal) if principal.verify_secret(secret) => Ok(Some(principal.clone())), + Some(principal) if principal.verify_secret(secret).await => Ok(Some(principal.clone())), _ => Ok(None), } } @@ -114,10 +114,13 @@ impl Directory for MemoryDirectory { } async fn is_local_domain(&self, domain: &str) -> crate::Result { - let domain = format!("@{domain}"); - Ok(self - .emails_to_ids - .keys() - .any(|email| email.ends_with(&domain))) + Ok(if !self.domains.contains(domain) { + let domain = format!("@{domain}"); + self.emails_to_ids + .keys() + .any(|email| email.ends_with(&domain)) + } else { + true + }) } } diff --git a/crates/directory/src/memory/mod.rs b/crates/directory/src/memory/mod.rs index 846c7a42..e2dcca39 100644 --- a/crates/directory/src/memory/mod.rs +++ b/crates/directory/src/memory/mod.rs @@ -1,4 +1,4 @@ -use ahash::AHashMap; +use ahash::{AHashMap, AHashSet}; use crate::Principal; @@ -11,6 +11,7 @@ pub struct MemoryDirectory { names: AHashMap, emails_to_ids: AHashMap>>, ids_to_email: AHashMap>>, + domains: AHashSet, } enum EmailType { diff --git a/crates/directory/src/secret.rs b/crates/directory/src/secret.rs index d9841e3d..58ee0ed0 100644 --- a/crates/directory/src/secret.rs +++ b/crates/directory/src/secret.rs @@ -9,25 +9,39 @@ use sha1::Digest; use sha1::Sha1; use sha2::Sha256; use sha2::Sha512; +use tokio::sync::oneshot; use crate::Principal; impl Principal { - pub fn verify_secret(&self, secret: &str) -> bool { - self.secrets.iter().any(|s| verify_secret_hash(s, secret)) + pub async fn verify_secret(&self, secret: &str) -> bool { + for hashed_secret in &self.secrets { + if verify_secret_hash(hashed_secret, secret).await { + return true; + } + } + false } } -fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool { +async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool { if hashed_secret.starts_with('$') { if hashed_secret.starts_with("$argon2") || hashed_secret.starts_with("$pbkdf2") || hashed_secret.starts_with("$scrypt") { - match PasswordHash::new(hashed_secret) { - Ok(hash) => hash - .verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], secret) - .is_ok(), + let (tx, rx) = oneshot::channel(); + let secret = secret.to_string(); + let hashed_secret = hashed_secret.to_string(); + + tokio::task::spawn_blocking(move || match PasswordHash::new(&hashed_secret) { + Ok(hash) => { + tx.send( + hash.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret) + .is_ok(), + ) + .ok(); + } Err(_) => { tracing::warn!( context = "directory", @@ -35,6 +49,14 @@ fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool { hash = hashed_secret, "Invalid password hash" ); + tx.send(false).ok(); + } + }); + + match rx.await { + Ok(result) => result, + Err(_) => { + tracing::warn!(context = "directory", event = "error", "Thread join error"); false } } diff --git a/crates/directory/src/smtp/config.rs b/crates/directory/src/smtp/config.rs index 88262800..cfba988c 100644 --- a/crates/directory/src/smtp/config.rs +++ b/crates/directory/src/smtp/config.rs @@ -1,10 +1,14 @@ use std::sync::Arc; -use ahash::AHashSet; use mail_send::{smtp::tls::build_tls_connector, SmtpClientBuilder}; use utils::config::{utils::AsKey, Config}; -use crate::{cache::CachedDirectory, config::build_pool, smtp::SmtpConnectionManager, Directory}; +use crate::{ + cache::CachedDirectory, + config::{build_pool, ConfigDirectory}, + smtp::SmtpConnectionManager, + Directory, +}; use super::SmtpDirectory; @@ -12,7 +16,6 @@ impl SmtpDirectory { pub fn from_config( config: &Config, prefix: impl AsKey, - domains: AHashSet, is_lmtp: bool, ) -> utils::config::Result> { let prefix = prefix.as_key(); @@ -46,7 +49,7 @@ impl SmtpDirectory { &prefix, SmtpDirectory { pool: build_pool(config, &prefix, manager)?, - domains, + domains: config.parse_lookup_list((&prefix, "lookup.domains"))?, }, ) } diff --git a/crates/directory/src/sql/config.rs b/crates/directory/src/sql/config.rs index 3abd4f6a..fb80cfd8 100644 --- a/crates/directory/src/sql/config.rs +++ b/crates/directory/src/sql/config.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use ahash::AHashSet; use sqlx::any::{install_default_drivers, AnyPoolOptions}; use utils::config::{utils::AsKey, Config}; @@ -12,7 +11,6 @@ impl SqlDirectory { pub fn from_config( config: &Config, prefix: impl AsKey, - domains: AHashSet, ) -> utils::config::Result> { let prefix = prefix.as_key(); let address = config.value_require((&prefix, "address"))?; @@ -95,14 +93,6 @@ impl SqlDirectory { .to_string(), }; - CachedDirectory::try_from_config( - config, - &prefix, - SqlDirectory { - pool, - mappings, - domains, - }, - ) + CachedDirectory::try_from_config(config, &prefix, SqlDirectory { pool, mappings }) } } diff --git a/crates/directory/src/sql/lookup.rs b/crates/directory/src/sql/lookup.rs index b4e86bf4..faa45410 100644 --- a/crates/directory/src/sql/lookup.rs +++ b/crates/directory/src/sql/lookup.rs @@ -22,13 +22,11 @@ impl Directory for SqlDirectory { .fetch_optional(&self.pool) .await? { - self.mappings.row_to_principal(row).map(|p| { - if p.verify_secret(secret) { - Some(p) - } else { - None - } - }) + match self.mappings.row_to_principal(row) { + Ok(principal) if principal.verify_secret(secret).await => Ok(Some(principal)), + Ok(_) => Ok(None), + Err(err) => Err(err), + } } else { Ok(None) } @@ -122,10 +120,6 @@ impl Directory for SqlDirectory { } async fn is_local_domain(&self, domain: &str) -> crate::Result { - if self.domains.contains(domain) { - return Ok(true); - } - sqlx::query(&self.mappings.query_domains) .bind(domain) .fetch_optional(&self.pool) @@ -142,26 +136,25 @@ impl SqlMappings { ..Default::default() }; for col in row.columns() { - let name = col.name(); let idx = col.ordinal(); + let name = col.name(); + if name.eq_ignore_ascii_case(&self.column_id) { principal.id = row.try_get::(idx)? as u32; } else if name.eq_ignore_ascii_case(&self.column_name) { - principal.name = row.try_get::, _>(idx)?.unwrap_or_default(); + principal.name = row.try_get::(idx)?; } else if name.eq_ignore_ascii_case(&self.column_secret) { - if let Some(secret) = row.try_get::, _>(idx)? { + if let Ok(secret) = row.try_get::(idx) { principal.secrets.push(secret); } } else if name.eq_ignore_ascii_case(&self.column_type) { - if let Some(typ) = row.try_get::, _>(idx)? { - match typ.as_str() { - "individual" | "person" | "user" => principal.typ = Type::Individual, - "group" => principal.typ = Type::Group, - _ => (), - } + match row.try_get::(idx)?.as_str() { + "individual" | "person" | "user" => principal.typ = Type::Individual, + "group" => principal.typ = Type::Group, + _ => (), } } else if name.eq_ignore_ascii_case(&self.column_description) { - principal.description = row.try_get::, _>(idx)?; + principal.description = row.try_get::(idx).ok(); } else if name.eq_ignore_ascii_case(&self.column_quota) { principal.quota = row.try_get::(idx).unwrap_or_default() as u32; } diff --git a/crates/directory/src/sql/mod.rs b/crates/directory/src/sql/mod.rs index 79b59f98..2e6f0433 100644 --- a/crates/directory/src/sql/mod.rs +++ b/crates/directory/src/sql/mod.rs @@ -1,4 +1,3 @@ -use ahash::AHashSet; use sqlx::{Any, Pool}; pub mod config; @@ -7,7 +6,6 @@ pub mod lookup; pub struct SqlDirectory { pool: Pool, mappings: SqlMappings, - domains: AHashSet, } #[derive(Debug)] diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs index cda9c9b9..8274aa2c 100644 --- a/crates/jmap/src/api/event_source.rs +++ b/crates/jmap/src/api/event_source.rs @@ -34,7 +34,7 @@ use hyper::{ use jmap_proto::{error::request::RequestError, types::type_state::TypeState}; use utils::map::bitmap::Bitmap; -use crate::{auth::AclToken, JMAP, LONG_SLUMBER}; +use crate::{auth::AccessToken, JMAP, LONG_SLUMBER}; use super::{http::ToHttpResponse, HttpRequest, HttpResponse, StateChangeResponse}; @@ -48,7 +48,7 @@ impl JMAP { pub async fn handle_event_source( &self, req: HttpRequest, - acl_token: Arc, + access_token: Arc, ) -> HttpResponse { // Parse query let mut ping = 0; @@ -110,7 +110,7 @@ impl JMAP { // Register with state manager let mut change_rx = if let Some(change_rx) = self - .subscribe_state_manager(acl_token.primary_id(), acl_token.primary_id(), types) + .subscribe_state_manager(access_token.primary_id(), access_token.primary_id(), types) .await { change_rx diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 7735d607..10daf58f 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -50,7 +50,7 @@ use crate::{ blob::{DownloadResponse, UploadResponse}, services::state, websocket::upgrade::upgrade_websocket_connection, - JMAP, SUPERUSER_ID, + JMAP, }; use super::{ @@ -69,7 +69,8 @@ pub async fn parse_jmap_request( match path.next().unwrap_or("") { "jmap" => { // Authenticate request - let (_in_flight, acl_token) = match jmap.authenticate_headers(&req, remote_ip).await { + let (_in_flight, access_token) = match jmap.authenticate_headers(&req, remote_ip).await + { Ok(Some(session)) => session, Ok(None) => return RequestError::unauthorized().into_http_response(), Err(err) => return err.into_http_response(), @@ -89,7 +90,7 @@ pub async fn parse_jmap_request( Ok(request) => { //let _ = println!("<- {}", String::from_utf8_lossy(&bytes)); - match jmap.handle_request(request, acl_token, &instance).await { + match jmap.handle_request(request, access_token, &instance).await { Ok(response) => response.into_http_response(), Err(err) => err.into_http_response(), } @@ -103,7 +104,7 @@ pub async fn parse_jmap_request( path.next().and_then(BlobId::from_base32), path.next(), ) { - return match jmap.blob_download(&blob_id, &acl_token).await { + return match jmap.blob_download(&blob_id, &access_token).await { Ok(Some(blob)) => DownloadResponse { filename: name.to_string(), content_type: req @@ -136,7 +137,7 @@ pub async fn parse_jmap_request( .and_then(|h| h.to_str().ok()) .unwrap_or("application/octet-stream"), &bytes, - acl_token, + access_token, ) .await { @@ -149,10 +150,10 @@ pub async fn parse_jmap_request( } } ("eventsource", &Method::GET) => { - return jmap.handle_event_source(req, acl_token).await + return jmap.handle_event_source(req, access_token).await } ("ws", &Method::GET) => { - return upgrade_websocket_connection(jmap, req, acl_token, instance.clone()) + return upgrade_websocket_connection(jmap, req, access_token, instance.clone()) .await; } _ => (), @@ -161,14 +162,14 @@ pub async fn parse_jmap_request( ".well-known" => match (path.next().unwrap_or(""), req.method()) { ("jmap", &Method::GET) => { // Authenticate request - let (_in_flight, acl_token) = match jmap.authenticate_headers(&req, remote_ip).await - { - Ok(Some(session)) => session, - Ok(None) => return RequestError::unauthorized().into_http_response(), - Err(err) => return err.into_http_response(), - }; + let (_in_flight, access_token) = + match jmap.authenticate_headers(&req, remote_ip).await { + Ok(Some(session)) => session, + Ok(None) => return RequestError::unauthorized().into_http_response(), + Err(err) => return err.into_http_response(), + }; - return match jmap.handle_session_resource(instance, acl_token).await { + return match jmap.handle_session_resource(instance, access_token).await { Ok(session) => session.into_http_response(), Err(err) => err.into_http_response(), }; @@ -232,7 +233,7 @@ pub async fn parse_jmap_request( "admin" => { // Make sure the user is a superuser match jmap.authenticate_headers(&req, remote_ip).await { - Ok(Some((_, acl_token))) if acl_token.primary_id() == SUPERUSER_ID => (), + Ok(Some((_, access_token))) if access_token.is_super_user() => (), Ok(_) => return RequestError::unauthorized().into_http_response(), Err(err) => return err.into_http_response(), } @@ -455,16 +456,14 @@ impl HtmlResponse { impl ToHttpResponse for Response { fn into_http_response(self) -> HttpResponse { - //let delete = ""; - //println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); + //let _ = println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); JsonResponse::new(self).into_http_response() } } impl ToHttpResponse for Session { fn into_http_response(self) -> HttpResponse { - //let delete = ""; - //println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); + //let _ = println!("-> {}", serde_json::to_string_pretty(&self).unwrap()); JsonResponse::new(self).into_http_response() } } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index f863d980..53d61edc 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -35,17 +35,17 @@ use jmap_proto::{ }; use utils::listener::ServerInstance; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn handle_request( &self, request: Request, - acl_token: Arc, + access_token: Arc, instance: &Arc, ) -> Result { let mut response = Response::new( - acl_token.state(), + access_token.state(), request.created_ids.unwrap_or_default(), request.method_calls.len(), ); @@ -63,7 +63,7 @@ impl JMAP { // Add response match self - .handle_method_call(call.method, &acl_token, &mut next_call, instance) + .handle_method_call(call.method, &access_token, &mut next_call, instance) .await { Ok(mut method_response) => { @@ -122,149 +122,149 @@ impl JMAP { async fn handle_method_call( &self, method: RequestMethod, - acl_token: &AclToken, + access_token: &AccessToken, next_call: &mut Option>, instance: &Arc, ) -> Result { Ok(match method { RequestMethod::Get(mut req) => match req.take_arguments() { get::RequestArguments::Email(arguments) => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_get(req.with_arguments(arguments), acl_token) + self.email_get(req.with_arguments(arguments), access_token) .await? .into() } get::RequestArguments::Mailbox => { - acl_token.assert_has_access(req.account_id, Collection::Mailbox)?; + access_token.assert_has_access(req.account_id, Collection::Mailbox)?; - self.mailbox_get(req, acl_token).await?.into() + self.mailbox_get(req, access_token).await?.into() } get::RequestArguments::Thread => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; self.thread_get(req).await?.into() } get::RequestArguments::Identity => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.identity_get(req).await?.into() } get::RequestArguments::EmailSubmission => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.email_submission_get(req).await?.into() } get::RequestArguments::PushSubscription => { - self.push_subscription_get(req, acl_token).await?.into() + self.push_subscription_get(req, access_token).await?.into() } get::RequestArguments::SieveScript => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.sieve_script_get(req).await?.into() } get::RequestArguments::VacationResponse => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.vacation_response_get(req).await?.into() } }, RequestMethod::Query(mut req) => match req.take_arguments() { query::RequestArguments::Email(arguments) => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_query(req.with_arguments(arguments), acl_token) + self.email_query(req.with_arguments(arguments), access_token) .await? .into() } query::RequestArguments::Mailbox(arguments) => { - acl_token.assert_has_access(req.account_id, Collection::Mailbox)?; + access_token.assert_has_access(req.account_id, Collection::Mailbox)?; - self.mailbox_query(req.with_arguments(arguments), acl_token) + self.mailbox_query(req.with_arguments(arguments), access_token) .await? .into() } query::RequestArguments::EmailSubmission => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.email_submission_query(req).await?.into() } query::RequestArguments::SieveScript => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.sieve_script_query(req).await?.into() } }, RequestMethod::Set(mut req) => match req.take_arguments() { set::RequestArguments::Email => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_set(req, acl_token).await?.into() + self.email_set(req, access_token).await?.into() } set::RequestArguments::Mailbox(arguments) => { - acl_token.assert_has_access(req.account_id, Collection::Mailbox)?; + access_token.assert_has_access(req.account_id, Collection::Mailbox)?; - self.mailbox_set(req.with_arguments(arguments), acl_token) + self.mailbox_set(req.with_arguments(arguments), access_token) .await? .into() } set::RequestArguments::Identity => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.identity_set(req).await?.into() } set::RequestArguments::EmailSubmission(arguments) => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.email_submission_set(req.with_arguments(arguments), instance, next_call) .await? .into() } set::RequestArguments::PushSubscription => { - self.push_subscription_set(req, acl_token).await?.into() + self.push_subscription_set(req, access_token).await?.into() } set::RequestArguments::SieveScript(arguments) => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; - self.sieve_script_set(req.with_arguments(arguments), acl_token) + self.sieve_script_set(req.with_arguments(arguments), access_token) .await? .into() } set::RequestArguments::VacationResponse => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; self.vacation_response_set(req).await?.into() } }, - RequestMethod::Changes(req) => self.changes(req, acl_token).await?.into(), + RequestMethod::Changes(req) => self.changes(req, access_token).await?.into(), RequestMethod::Copy(req) => { - acl_token + access_token .assert_has_access(req.account_id, Collection::Email)? .assert_has_access(req.from_account_id, Collection::Email)?; - self.email_copy(req, acl_token, next_call).await?.into() + self.email_copy(req, access_token, next_call).await?.into() } - RequestMethod::CopyBlob(req) => self.blob_copy(req, acl_token).await?.into(), + RequestMethod::CopyBlob(req) => self.blob_copy(req, access_token).await?.into(), RequestMethod::ImportEmail(req) => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_import(req, acl_token).await?.into() + self.email_import(req, access_token).await?.into() } RequestMethod::ParseEmail(req) => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_parse(req, acl_token).await?.into() + self.email_parse(req, access_token).await?.into() } - RequestMethod::QueryChanges(req) => self.query_changes(req, acl_token).await?.into(), + RequestMethod::QueryChanges(req) => self.query_changes(req, access_token).await?.into(), RequestMethod::SearchSnippet(req) => { - acl_token.assert_has_access(req.account_id, Collection::Email)?; + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_search_snippet(req, acl_token).await?.into() + self.email_search_snippet(req, access_token).await?.into() } RequestMethod::ValidateScript(req) => { - acl_token.assert_is_member(req.account_id)?; + access_token.assert_is_member(req.account_id)?; - self.sieve_script_validate(req, acl_token).await?.into() + self.sieve_script_validate(req, access_token).await?.into() } RequestMethod::Echo(req) => req.into(), RequestMethod::Error(error) => return Err(error), diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 15f5ee96..d323517b 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -32,7 +32,7 @@ use jmap_proto::{ use store::ahash::AHashSet; use utils::{listener::ServerInstance, map::vec_map::VecMap, UnwrapFailure}; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; #[derive(Debug, Clone, serde::Serialize)] pub struct Session { @@ -167,34 +167,36 @@ impl JMAP { pub async fn handle_session_resource( &self, instance: Arc, - acl_token: Arc, + access_token: Arc, ) -> Result { let mut session = Session::new(&instance.data, &self.config.capabilities); - session.set_state(acl_token.state()); - let account_name = self - .get_account_login(acl_token.primary_id()) - .await - .unwrap_or_else(|| Id::from(acl_token.primary_id()).to_string()); + session.set_state(access_token.state()); session.set_primary_account( - acl_token.primary_id().into(), - account_name.to_string(), - account_name, + access_token.primary_id().into(), + access_token.name.clone(), + access_token + .description + .clone() + .unwrap_or_else(|| access_token.name.clone()), None, ); // Add secondary accounts - for id in acl_token.secondary_ids() { - let is_personal = !acl_token.is_member(*id); + for id in access_token.secondary_ids() { + let is_personal = !access_token.is_member(*id); let is_readonly = is_personal && self - .shared_documents(&acl_token, *id, Collection::Mailbox, Acl::AddItems) + .shared_documents(&access_token, *id, Collection::Mailbox, Acl::AddItems) .await .map_or(true, |ids| ids.is_empty()); session.add_account( (*id).into(), - self.get_account_login(*id) + self.directory + .principal_by_id(*id) .await + .unwrap_or_default() + .map(|p| p.name) .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, is_readonly, diff --git a/crates/jmap/src/auth/account.rs b/crates/jmap/src/auth/account.rs deleted file mode 100644 index 4a58123a..00000000 --- a/crates/jmap/src/auth/account.rs +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright (c) 2023 Stalwart Labs Ltd. - * - * This file is part of the Stalwart JMAP Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * in the LICENSE file at the top-level directory of this distribution. - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * You can be released from the requirements of the AGPLv3 license by - * purchasing a commercial license. Please contact licensing@stalw.art - * for more details. -*/ - -use crate::JMAP; - -use super::AclToken; - -impl JMAP { - pub async fn authenticate(&self, account: &str, secret: &str) -> Option { - todo!() - } - - pub async fn authenticate_with_token(&self, account: &str, secret: &str) -> Option { - todo!() - } - - pub async fn get_acl_token(&self, account_id: u32) -> Option { - todo!() - } - - pub async fn get_account_secret(&self, account_id: u32) -> Option { - todo!() - } - - pub async fn get_account_name(&self, account_id: u32) -> Option { - todo!() - } - - pub async fn get_account_id(&self, account: &str) -> Option { - todo!() - } - - pub async fn get_account_gids(&self, account_id: u32) -> Vec { - todo!() - } - - pub async fn get_account_login(&self, account_id: u32) -> Option { - todo!() - } - - pub async fn get_uids_by_address(&self, address: &str) -> Vec { - todo!() - } - - pub async fn get_addresses_by_uid(&self, account_id: u32) -> Vec { - todo!() - } - - pub async fn vrfy_address(&self, address: &str) -> Vec { - todo!() - } - - pub async fn expn_address(&self, address: &str) -> Vec { - todo!() - } -} - -struct Remove { - remove: bool, -} - -/* -// TODO abstract this -impl SqlDatabase { - pub async fn fetch_uid_to_string(&self, query: &str, uid: i64) -> Option { - let result = match &self { - SqlDatabase::Postgres(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_optional(pool) - .await - } - SqlDatabase::MySql(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_optional(pool) - .await - } - /*SqlDatabase::MsSql(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_optional(pool) - .await - }*/ - SqlDatabase::SqlLite(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_optional(pool) - .await - } - }; - - match result { - Ok(result) => result, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - None - } - } - } - - pub async fn fetch_string_to_id(&self, query: &str, param: &str) -> Option { - let result = match &self { - SqlDatabase::Postgres(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_optional(pool) - .await - } - SqlDatabase::MySql(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_optional(pool) - .await - } - /*SqlDatabase::MsSql(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_optional(pool) - .await - }*/ - SqlDatabase::SqlLite(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_optional(pool) - .await - } - }; - - match result { - Ok(result) => result, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - None - } - } - } - - pub async fn fetch_uid_to_strings(&self, query: &str, uid: i64) -> Vec { - let result = match &self { - SqlDatabase::Postgres(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_all(pool) - .await - } - SqlDatabase::MySql(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_all(pool) - .await - } - /*SqlDatabase::MsSql(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_all(pool) - .await - }*/ - SqlDatabase::SqlLite(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(uid) - .fetch_all(pool) - .await - } - }; - - match result { - Ok(result) => result, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - vec![] - } - } - } - - pub async fn fetch_uid_to_uids(&self, query: &str, uid: i64) -> Vec { - let result = match &self { - SqlDatabase::Postgres(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(uid) - .fetch_all(pool) - .await - } - SqlDatabase::MySql(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(uid) - .fetch_all(pool) - .await - } - /*SqlDatabase::MsSql(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(uid) - .fetch_all(pool) - .await - }*/ - SqlDatabase::SqlLite(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(uid) - .fetch_all(pool) - .await - } - }; - - match result { - Ok(result) => result, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - vec![] - } - } - } - - pub async fn fetch_string_to_uids(&self, query: &str, param: &str) -> Vec { - let result = match &self { - SqlDatabase::Postgres(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_all(pool) - .await - } - SqlDatabase::MySql(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_all(pool) - .await - } - /*SqlDatabase::MsSql(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_all(pool) - .await - }*/ - SqlDatabase::SqlLite(pool) => { - sqlx::query_scalar::<_, i64>(query) - .bind(param) - .fetch_all(pool) - .await - } - }; - - match result { - Ok(result) => result, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - vec![] - } - } - } - - pub async fn fetch_string_to_strings(&self, query: &str, param: &str) -> Vec { - let result = match &self { - SqlDatabase::Postgres(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(param) - .fetch_all(pool) - .await - } - SqlDatabase::MySql(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(param) - .fetch_all(pool) - .await - } - /*SqlDatabase::MsSql(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(param) - .fetch_all(pool) - .await - }*/ - SqlDatabase::SqlLite(pool) => { - sqlx::query_scalar::<_, String>(query) - .bind(param) - .fetch_all(pool) - .await - } - }; - - match result { - Ok(result) => result, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - vec![] - } - } - } - - pub async fn execute(&self, query: &str, params: impl Iterator) -> bool { - let result = match self { - SqlDatabase::Postgres(pool) => { - let mut q = sqlx::query(query); - for param in params { - q = q.bind(param); - } - q.execute(pool).await.map(|_| ()) - } - SqlDatabase::MySql(pool) => { - let mut q = sqlx::query(query); - for param in params { - q = q.bind(param); - } - q.execute(pool).await.map(|_| ()) - } - /*SqlDatabase::MsSql(pool) => { - let mut q = sqlx::query(query); - for param in params { - q = q.bind(param); - } - q.execute(pool).await.map(|_| ()) - }*/ - SqlDatabase::SqlLite(pool) => { - let mut q = sqlx::query(query); - for param in params { - q = q.bind(param); - } - q.execute(pool).await.map(|_| ()) - } - }; - - match result { - Ok(_) => true, - Err(err) => { - tracing::warn!(context = "sql", event = "error", query = query, reason = ?err); - false - } - } - } -} - -#[cfg(feature = "test_mode")] -impl AuthDatabase { - pub async fn execute(&self, query: &str, params: impl Iterator) -> bool { - match self { - AuthDatabase::Sql { db, .. } => db.execute(query, params).await, - AuthDatabase::Ldap => unimplemented!(), - } - } -} -*/ diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index ce833700..3e5e9311 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -27,7 +27,6 @@ use jmap_proto::{ types::{ acl::Acl, collection::Collection, - id::Id, property::Property, value::{MaybePatchValue, Value}, }, @@ -39,15 +38,15 @@ use store::{ }; use utils::map::bitmap::{Bitmap, BitmapItem}; -use crate::{JMAP, SUPERUSER_ID}; +use crate::JMAP; -use super::AclToken; +use super::AccessToken; impl JMAP { - pub async fn update_acl_token(&self, mut acl_token: AclToken) -> Option { - for &grant_account_id in [acl_token.primary_id] + pub async fn update_access_token(&self, mut access_token: AccessToken) -> Option { + for &grant_account_id in [access_token.primary_id] .iter() - .chain(acl_token.member_of.clone().iter()) + .chain(access_token.member_of.clone().iter()) { let from_key = AclKey { grant_account_id, @@ -64,14 +63,14 @@ impl JMAP { match self .store .iterate( - acl_token, + access_token, from_key, to_key, false, true, - |acl_token, key, value| { + |access_token, key, value| { let acl_key = AclKey::deserialize(key)?; - if acl_token.is_member(acl_key.to_account_id) { + if access_token.is_member(acl_key.to_account_id) { return Ok(true); } @@ -94,14 +93,14 @@ impl JMAP { } if !collections.is_empty() { - if let Some((_, sharing)) = acl_token + if let Some((_, sharing)) = access_token .access_to .iter_mut() .find(|(account_id, _)| *account_id == acl_key.to_account_id) { sharing.union(&collections); } else { - acl_token + access_token .access_to .push((acl_key.to_account_id, collections)); } @@ -112,8 +111,8 @@ impl JMAP { ) .await { - Ok(acl_token_) => { - acl_token = acl_token_; + Ok(access_token_) => { + access_token = access_token_; } Err(err) => { tracing::error!( @@ -125,12 +124,12 @@ impl JMAP { } } } - acl_token.into() + access_token.into() } pub async fn shared_documents( &self, - acl_token: &AclToken, + access_token: &AccessToken, to_account_id: u32, to_collection: Collection, check_acls: impl Into>, @@ -138,9 +137,9 @@ impl JMAP { let check_acls = check_acls.into(); let mut document_ids = RoaringBitmap::new(); let to_collection = u8::from(to_collection); - for &grant_account_id in [acl_token.primary_id] + for &grant_account_id in [access_token.primary_id] .iter() - .chain(acl_token.member_of.clone().iter()) + .chain(access_token.member_of.clone().iter()) { let from_key = AclKey { grant_account_id, @@ -193,13 +192,13 @@ impl JMAP { pub async fn shared_messages( &self, - acl_token: &AclToken, + access_token: &AccessToken, to_account_id: u32, check_acls: impl Into>, ) -> Result { let check_acls = check_acls.into(); let shared_mailboxes = self - .shared_documents(acl_token, to_account_id, Collection::Mailbox, check_acls) + .shared_documents(access_token, to_account_id, Collection::Mailbox, check_acls) .await?; if shared_mailboxes.is_empty() { return Ok(shared_mailboxes); @@ -224,7 +223,7 @@ impl JMAP { pub async fn owned_or_shared_documents( &self, - acl_token: &AclToken, + access_token: &AccessToken, account_id: u32, collection: Collection, check_acls: impl Into>, @@ -234,9 +233,9 @@ impl JMAP { .get_document_ids(account_id, collection) .await? .unwrap_or_default(); - if !document_ids.is_empty() && !acl_token.is_member(account_id) { + if !document_ids.is_empty() && !access_token.is_member(account_id) { document_ids &= self - .shared_documents(acl_token, account_id, collection, check_acls) + .shared_documents(access_token, account_id, collection, check_acls) .await?; } Ok(document_ids) @@ -244,7 +243,7 @@ impl JMAP { pub async fn owned_or_shared_messages( &self, - acl_token: &AclToken, + access_token: &AccessToken, account_id: u32, check_acls: impl Into>, ) -> Result { @@ -253,9 +252,9 @@ impl JMAP { .get_document_ids(account_id, Collection::Email) .await? .unwrap_or_default(); - if !document_ids.is_empty() && !acl_token.is_member(account_id) { + if !document_ids.is_empty() && !access_token.is_member(account_id) { document_ids &= self - .shared_messages(acl_token, account_id, check_acls) + .shared_messages(access_token, account_id, check_acls) .await?; } Ok(document_ids) @@ -263,7 +262,7 @@ impl JMAP { pub async fn has_access_to_document( &self, - acl_token: &AclToken, + access_token: &AccessToken, to_account_id: u32, to_collection: impl Into, to_document_id: u32, @@ -271,9 +270,9 @@ impl JMAP { ) -> Result { let to_collection = to_collection.into(); let check_acls = check_acls.into(); - for &grant_account_id in [acl_token.primary_id] + for &grant_account_id in [access_token.primary_id] .iter() - .chain(acl_token.member_of.clone().iter()) + .chain(access_token.member_of.clone().iter()) { match self .store @@ -399,10 +398,15 @@ impl JMAP { Ok(()) } - pub async fn acl_get(&self, value: &[Value], acl_token: &AclToken, account_id: u32) -> Value { - if acl_token.is_member(account_id) + pub async fn acl_get( + &self, + value: &[Value], + access_token: &AccessToken, + account_id: u32, + ) -> Value { + if access_token.is_member(account_id) || value.chunks_exact(2).any(|item| { - acl_token.is_member( + access_token.is_member( item.first() .and_then(|v| v.as_id().map(|id| id.document_id())) .unwrap_or(u32::MAX), @@ -415,7 +419,19 @@ impl JMAP { if let (Some(Value::Id(id)), Some(Value::UnsignedInt(acl_bits))) = (item.first(), item.last()) { - if let Some(account_name) = self.get_account_login(id.document_id()).await { + if let Some(account_name) = self + .directory + .principal_by_id(id.document_id()) + .await + .unwrap_or_default() + .and_then(|p| { + if p.has_name() { + Some(p.name().to_string()) + } else { + None + } + }) + { acl_obj.append( Property::_T(account_name), Bitmap::::from(*acl_bits) @@ -438,7 +454,7 @@ impl JMAP { current: &Option>>, ) { if let Value::List(acl_changes) = changes.get(&Property::Acl) { - let mut acl_tokens = self.acl_tokens.lock(); + let mut access_tokens = self.access_tokens.lock(); if let Some(Value::List(acl_current)) = current .as_ref() .and_then(|current| current.inner.properties.get(&Property::Acl)) @@ -453,7 +469,7 @@ impl JMAP { } if invalidate { if let Some(Value::Id(id)) = current_item.first() { - acl_tokens.remove(&id.document_id()); + access_tokens.remove(&id.document_id()); } } } @@ -468,14 +484,14 @@ impl JMAP { } if invalidate { if let Some(Value::Id(id)) = change_item.first() { - acl_tokens.remove(&id.document_id()); + access_tokens.remove(&id.document_id()); } } } } else { for value in acl_changes { if let Value::Id(id) = value { - acl_tokens.remove(&id.document_id()); + access_tokens.remove(&id.document_id()); } } } @@ -485,12 +501,20 @@ impl JMAP { async fn map_acl_accounts(&self, mut acl_set: Vec) -> Result, SetError> { for item in &mut acl_set { if let Value::Text(account_name) = item { - if let Some(account_id) = self.get_account_id(account_name).await { - *item = Value::Id(account_id.into()); - } else { - return Err(SetError::invalid_properties() - .with_property(Property::Acl) - .with_description(format!("Account {account_name} does not exist."))); + match self.directory.principal_by_name(account_name).await { + Ok(Some(principal)) if principal.has_id() => { + *item = Value::Id(principal.id().into()); + } + Ok(None) => { + return Err(SetError::invalid_properties() + .with_property(Property::Acl) + .with_description(format!("Account {account_name} does not exist."))); + } + _ => { + return Err(SetError::forbidden() + .with_property(Property::Acl) + .with_description("Temporary server failure during lookup")); + } } } } @@ -499,76 +523,19 @@ impl JMAP { } } -impl AclToken { - pub fn primary_id(&self) -> u32 { - self.primary_id - } - - pub fn secondary_ids(&self) -> impl Iterator { - self.member_of - .iter() - .chain(self.access_to.iter().map(|(id, _)| id)) - } - - pub fn is_member(&self, account_id: u32) -> bool { - self.primary_id == account_id - || self.member_of.contains(&account_id) - || self.primary_id == SUPERUSER_ID - || self.member_of.contains(&SUPERUSER_ID) - } - - pub fn is_shared(&self, account_id: u32) -> bool { - !self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id) - } - - pub fn has_access(&self, to_account_id: u32, to_collection: impl Into) -> bool { - let to_collection = to_collection.into(); - self.is_member(to_account_id) - || self.access_to.iter().any(|(id, collections)| { - *id == to_account_id && collections.contains(to_collection) - }) - } - - pub fn assert_has_access( - &self, - to_account_id: Id, - to_collection: Collection, - ) -> Result<&Self, MethodError> { - if self.has_access(to_account_id.document_id(), to_collection) { - Ok(self) - } else { - Err(MethodError::Forbidden(format!( - "You do not have access to account {}", - to_account_id - ))) - } - } - - pub fn assert_is_member(&self, account_id: Id) -> Result<&Self, MethodError> { - if self.is_member(account_id.document_id()) { - Ok(self) - } else { - Err(MethodError::Forbidden(format!( - "You are not an owner of account {}", - account_id - ))) - } - } -} - pub trait EffectiveAcl { - fn effective_acl(&self, acl_token: &AclToken) -> Bitmap; + fn effective_acl(&self, access_token: &AccessToken) -> Bitmap; } impl EffectiveAcl for Object { - fn effective_acl(&self, acl_token: &AclToken) -> Bitmap { + fn effective_acl(&self, access_token: &AccessToken) -> Bitmap { let mut acl = Bitmap::::new(); if let Some(Value::List(permissions)) = self.properties.get(&Property::Acl) { for item in permissions.chunks(2) { if let (Some(Value::Id(account_id)), Some(Value::UnsignedInt(acl_bits))) = (item.first(), item.last()) { - if acl_token.is_member(account_id.document_id()) { + if access_token.is_member(account_id.document_id()) { acl.union(&Bitmap::from(*acl_bits)); } } diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 3fd9891b..308f8209 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -30,19 +30,19 @@ use std::{ use hyper::header; use jmap_proto::error::request::RequestError; use mail_parser::decoders::base64::base64_decode; -use mail_send::mail_auth::common::lru::DnsCache; +use mail_send::{mail_auth::common::lru::DnsCache, Credentials}; use utils::listener::limiter::InFlight; use crate::JMAP; -use super::{rate_limit::RemoteAddress, AclToken}; +use super::{rate_limit::RemoteAddress, AccessToken}; impl JMAP { pub async fn authenticate_headers( &self, req: &hyper::Request, remote_ip: IpAddr, - ) -> Result)>, RequestError> { + ) -> Result)>, RequestError> { if let Some((mechanism, token)) = req .headers() .get(header::AUTHORIZATION) @@ -50,18 +50,18 @@ impl JMAP { .and_then(|h| h.split_once(' ').map(|(l, t)| (l, t.trim().to_string()))) { let session = if let Some(account_id) = self.sessions.get(&token) { - if let Some(acl_token) = self.acl_tokens.get(&account_id) { - acl_token.into() + if let Some(access_token) = self.access_tokens.get(&account_id) { + access_token.into() } else { // Refresh ACL token - self.get_acl_token(account_id).await.map(|acl_token| { - let acl_token = Arc::new(acl_token); - self.acl_tokens.insert( + self.get_access_token(account_id).await.map(|access_token| { + let access_token = Arc::new(access_token); + self.access_tokens.insert( account_id, - acl_token.clone(), + access_token.clone(), Instant::now() + self.config.session_cache_ttl, ); - acl_token + access_token }) } } else { @@ -79,7 +79,7 @@ impl JMAP { }) }) { - self.authenticate_with_token(&account, &secret).await + self.authenticate_plain(&account, &secret).await } else { tracing::debug!( context = "authenticate_headers", @@ -93,7 +93,7 @@ impl JMAP { self.is_anonymous_allowed(addr)?; match self.validate_access_token("access_token", &token).await { - Ok((account_id, _, _)) => self.get_acl_token(account_id).await, + Ok((account_id, _, _)) => self.get_access_token(account_id).await, Err(err) => { tracing::debug!( context = "authenticate_headers", @@ -115,7 +115,7 @@ impl JMAP { session.primary_id(), Instant::now() + self.config.session_cache_ttl, ); - self.acl_tokens.insert( + self.access_tokens.insert( session.primary_id(), session.clone(), Instant::now() + self.config.session_cache_ttl, @@ -126,10 +126,7 @@ impl JMAP { if let Some(session) = session { // Enforce authenticated rate limit - Ok(Some(( - self.is_account_allowed(session.primary_id())?, - session, - ))) + Ok(Some((self.is_account_allowed(&session)?, session))) } else { Ok(None) } @@ -159,4 +156,52 @@ impl JMAP { RemoteAddress::IpAddress(Ipv4Addr::new(127, 0, 0, 1).into()) } } + + pub async fn authenticate_plain(&self, username: &str, secret: &str) -> Option { + let mut principal = self + .directory + .authenticate(&Credentials::Plain { + username: username.to_string(), + secret: secret.to_string(), + }) + .await + .ok()??; + if !principal.has_id() { + tracing::warn!( + context = "authenticate_plain", + username = username, + "Principal has no ID." + ); + return None; + } else if !principal.has_name() { + principal.name = username.to_string(); + } + // Obtain groups + let member_of = self + .directory + .member_of(&principal) + .await + .unwrap_or_default(); + + // Create access token + self.update_access_token(AccessToken::new(principal).with_member_of(member_of)) + .await + } + + pub async fn get_access_token(&self, id: u32) -> Option { + let mut principal = self.directory.principal_by_id(id).await.ok()??; + if !principal.has_id() { + principal.id = id; + } + // Obtain groups + let member_of = self + .directory + .member_of(&principal) + .await + .unwrap_or_default(); + + // Create access token + self.update_access_token(AccessToken::new(principal).with_member_of(member_of)) + .await + } } diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index 89f5fbc2..8244adfc 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -31,29 +31,40 @@ use aes_gcm_siv::{ AeadInPlace, Aes256GcmSiv, KeyInit, Nonce, }; -use jmap_proto::types::collection::Collection; +use directory::Principal; +use jmap_proto::{ + error::method::MethodError, + types::{collection::Collection, id::Id}, +}; use store::blake3; use utils::map::bitmap::Bitmap; -pub mod account; +use crate::SUPERUSER_ID; + pub mod acl; pub mod authenticate; pub mod oauth; pub mod rate_limit; #[derive(Debug, Clone)] -pub struct AclToken { +pub struct AccessToken { pub primary_id: u32, pub member_of: Vec, pub access_to: Vec<(u32, Bitmap)>, + pub name: String, + pub description: Option, + pub quota: u32, } -impl AclToken { - pub fn new(primary_id: u32) -> Self { +impl AccessToken { + pub fn new(principal: Principal) -> Self { Self { - primary_id, + primary_id: principal.id, member_of: Vec::new(), access_to: Vec::new(), + name: principal.name, + description: principal.description, + quota: principal.quota, } } @@ -72,6 +83,65 @@ impl AclToken { self.access_to.hash(&mut s); s.finish() as u32 } + + pub fn primary_id(&self) -> u32 { + self.primary_id + } + + pub fn secondary_ids(&self) -> impl Iterator { + self.member_of + .iter() + .chain(self.access_to.iter().map(|(id, _)| id)) + } + + pub fn is_member(&self, account_id: u32) -> bool { + self.primary_id == account_id + || self.member_of.contains(&account_id) + || self.primary_id == SUPERUSER_ID + || self.member_of.contains(&SUPERUSER_ID) + } + + pub fn is_super_user(&self) -> bool { + self.primary_id == SUPERUSER_ID || self.member_of.contains(&SUPERUSER_ID) + } + + pub fn is_shared(&self, account_id: u32) -> bool { + !self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id) + } + + pub fn has_access(&self, to_account_id: u32, to_collection: impl Into) -> bool { + let to_collection = to_collection.into(); + self.is_member(to_account_id) + || self.access_to.iter().any(|(id, collections)| { + *id == to_account_id && collections.contains(to_collection) + }) + } + + pub fn assert_has_access( + &self, + to_account_id: Id, + to_collection: Collection, + ) -> Result<&Self, MethodError> { + if self.has_access(to_account_id.document_id(), to_collection) { + Ok(self) + } else { + Err(MethodError::Forbidden(format!( + "You do not have access to account {}", + to_account_id + ))) + } + } + + pub fn assert_is_member(&self, account_id: Id) -> Result<&Self, MethodError> { + if self.is_member(account_id.document_id()) { + Ok(self) + } else { + Err(MethodError::Forbidden(format!( + "You are not an owner of account {}", + account_id + ))) + } + } } pub struct SymmetricEncrypt { diff --git a/crates/jmap/src/auth/oauth/device_auth.rs b/crates/jmap/src/auth/oauth/device_auth.rs index 79043c21..e87b6c68 100644 --- a/crates/jmap/src/auth/oauth/device_auth.rs +++ b/crates/jmap/src/auth/oauth/device_auth.rs @@ -171,7 +171,7 @@ impl JMAP { { if let (Some(email), Some(password)) = (fields.get("email"), fields.get("password")) { - if let Some(id) = self.authenticate_with_token(email, password).await { + if let Some(id) = self.authenticate_plain(email, password).await { oauth .account_id .store(id.primary_id(), atomic::Ordering::Relaxed); diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 8ececefa..271cb5e9 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -181,9 +181,15 @@ impl JMAP { with_refresh_token: bool, ) -> Result { let password_hash = self - .get_account_secret(account_id) + .directory + .principal_by_id(account_id) .await - .ok_or("Account no longer exists")?; + .map_err(|_| "Temporary lookup error")? + .ok_or("Account no longer exists")? + .secrets + .into_iter() + .next() + .ok_or("Failed to obtain password hash")?; Ok(TokenResponse::Granted { access_token: self.encode_access_token( @@ -293,9 +299,15 @@ impl JMAP { // Optain password hash let password_hash = self - .get_account_secret(account_id) + .directory + .principal_by_id(account_id) .await - .ok_or("Account no longer exists")?; + .map_err(|_| "Temporary lookup error")? + .ok_or("Account no longer exists")? + .secrets + .into_iter() + .next() + .ok_or("Failed to obtain password hash")?; // Build context let key = self.config.oauth_key.clone(); diff --git a/crates/jmap/src/auth/oauth/user_code.rs b/crates/jmap/src/auth/oauth/user_code.rs index 41e2089d..e55a9fb8 100644 --- a/crates/jmap/src/auth/oauth/user_code.rs +++ b/crates/jmap/src/auth/oauth/user_code.rs @@ -132,7 +132,7 @@ impl JMAP { // Authenticate user if let (Some(email), Some(password)) = (params.get("email"), params.get("password")) { - if let Some(acl_token) = self.authenticate_with_token(email, password).await { + if let Some(access_token) = self.authenticate_plain(email, password).await { // Generate client code let client_code = thread_rng() .sample_iter(Alphanumeric) @@ -145,7 +145,7 @@ impl JMAP { client_code.clone(), Arc::new(OAuthCode { status: STATUS_AUTHORIZED.into(), - account_id: acl_token.primary_id().into(), + account_id: access_token.primary_id().into(), client_id: code_req .get("client_id") .map(|s| s.as_str()) diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index 1d207489..d4b5cc9f 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -32,7 +32,9 @@ use mail_send::mail_auth::common::lru::DnsCache; use store::parking_lot::Mutex; use utils::listener::limiter::{ConcurrencyLimiter, InFlight, RateLimiter}; -use crate::{JMAP, SUPERUSER_ID}; +use crate::JMAP; + +use super::AccessToken; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum RemoteAddress { @@ -95,22 +97,22 @@ impl JMAP { }) } - pub fn is_account_allowed(&self, account_id: u32) -> Result { - if account_id != SUPERUSER_ID { - let limiter_ = self.get_authenticated_limiter(account_id); - let mut limiter = limiter_.lock(); + pub fn is_account_allowed(&self, access_token: &AccessToken) -> Result { + let limiter_ = self.get_authenticated_limiter(access_token.primary_id()); + let mut limiter = limiter_.lock(); - if limiter.request_limiter.is_allowed() { - if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() { - Ok(in_flight_request) - } else { - Err(RequestError::limit(RequestLimitError::Concurrent)) - } + if limiter.request_limiter.is_allowed() { + if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() { + Ok(in_flight_request) + } else if access_token.is_super_user() { + Ok(InFlight::default()) } else { - Err(RequestError::too_many_requests()) + Err(RequestError::limit(RequestLimitError::Concurrent)) } - } else { + } else if access_token.is_super_user() { Ok(InFlight::default()) + } else { + Err(RequestError::too_many_requests()) } } @@ -127,20 +129,18 @@ impl JMAP { } } - pub fn is_upload_allowed(&self, account_id: u32) -> Result { - if account_id != SUPERUSER_ID { - if let Some(in_flight_request) = self - .get_authenticated_limiter(account_id) - .lock() - .concurrent_uploads - .is_allowed() - { - Ok(in_flight_request) - } else { - Err(RequestError::limit(RequestLimitError::Concurrent)) - } - } else { + pub fn is_upload_allowed(&self, access_token: &AccessToken) -> Result { + if let Some(in_flight_request) = self + .get_authenticated_limiter(access_token.primary_id()) + .lock() + .concurrent_uploads + .is_allowed() + { + Ok(in_flight_request) + } else if access_token.is_super_user() { Ok(InFlight::default()) + } else { + Err(RequestError::limit(RequestLimitError::Concurrent)) } } diff --git a/crates/jmap/src/blob/copy.rs b/crates/jmap/src/blob/copy.rs index c09bea67..bd61d59f 100644 --- a/crates/jmap/src/blob/copy.rs +++ b/crates/jmap/src/blob/copy.rs @@ -32,13 +32,13 @@ use jmap_proto::{ use utils::map::vec_map::VecMap; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn blob_copy( &self, request: CopyBlobRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let mut response = CopyBlobResponse { from_account_id: request.from_account_id, @@ -49,7 +49,7 @@ impl JMAP { let account_id = request.account_id.document_id(); for blob_id in request.blob_ids { - if self.has_access_blob(&blob_id, acl_token).await? { + if self.has_access_blob(&blob_id, access_token).await? { let dest_blob_id = BlobId::temporary(account_id); match self .store diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs index d6243e1a..203f07c1 100644 --- a/crates/jmap/src/blob/download.rs +++ b/crates/jmap/src/blob/download.rs @@ -33,15 +33,15 @@ use mail_parser::{ }; use store::BlobKind; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn blob_download( &self, blob_id: &BlobId, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result>, MethodError> { - if !acl_token.is_member(blob_id.account_id()) { + if !access_token.is_member(blob_id.account_id()) { match &blob_id.kind { BlobKind::Linked { account_id, @@ -50,7 +50,7 @@ impl JMAP { } => { match self .has_access_to_document( - acl_token, + access_token, *account_id, *collection, *document_id, @@ -67,7 +67,7 @@ impl JMAP { document_id, } => { match self - .shared_messages(acl_token, *account_id, Acl::ReadItems) + .shared_messages(access_token, *account_id, Acl::ReadItems) .await { Ok(shared_messages) if shared_messages.contains(*document_id) => (), @@ -117,7 +117,7 @@ impl JMAP { pub async fn has_access_blob( &self, blob_id: &BlobId, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { Ok(match &blob_id.kind { BlobKind::Linked { @@ -125,11 +125,11 @@ impl JMAP { collection, document_id, } => { - acl_token.is_member(*account_id) - || (acl_token.has_access(*account_id, *collection) + access_token.is_member(*account_id) + || (access_token.has_access(*account_id, *collection) && self .has_access_to_document( - acl_token, + access_token, *account_id, *collection, *document_id, @@ -141,13 +141,13 @@ impl JMAP { account_id, document_id, } => { - acl_token.is_member(*account_id) + access_token.is_member(*account_id) || self - .shared_messages(acl_token, *account_id, Acl::ReadItems) + .shared_messages(access_token, *account_id, Acl::ReadItems) .await? .contains(*document_id) } - BlobKind::Temporary { account_id, .. } => acl_token.is_member(*account_id), + BlobKind::Temporary { account_id, .. } => access_token.is_member(*account_id), }) } } diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index f8e21893..3a6c9a87 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -29,7 +29,7 @@ use jmap_proto::{ }; use store::BlobKind; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; use super::UploadResponse; @@ -39,10 +39,10 @@ impl JMAP { account_id: Id, content_type: &str, data: &[u8], - acl_token: Arc, + access_token: Arc, ) -> Result { // Limit concurrent uploads - let _in_flight = self.is_upload_allowed(acl_token.primary_id())?; + let _in_flight = self.is_upload_allowed(&access_token)?; #[cfg(feature = "test_mode")] { diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index d8df3cbc..b09aabc0 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -28,37 +28,37 @@ use jmap_proto::{ }; use store::query::log::{Change, Changes, Query}; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn changes( &self, request: ChangesRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { // Map collection and validate ACLs let collection = match request.arguments { RequestArguments::Email => { - acl_token.assert_has_access(request.account_id, Collection::Email)?; + access_token.assert_has_access(request.account_id, Collection::Email)?; Collection::Email } RequestArguments::Mailbox => { - acl_token.assert_has_access(request.account_id, Collection::Mailbox)?; + access_token.assert_has_access(request.account_id, Collection::Mailbox)?; Collection::Mailbox } RequestArguments::Thread => { - acl_token.assert_has_access(request.account_id, Collection::Email)?; + access_token.assert_has_access(request.account_id, Collection::Email)?; Collection::Thread } RequestArguments::Identity => { - acl_token.assert_is_member(request.account_id)?; + access_token.assert_is_member(request.account_id)?; Collection::Identity } RequestArguments::EmailSubmission => { - acl_token.assert_is_member(request.account_id)?; + access_token.assert_is_member(request.account_id)?; Collection::EmailSubmission } diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index 4c440fb1..3bc04166 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -30,13 +30,13 @@ use jmap_proto::{ }, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn query_changes( &self, request: QueryChangesRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { // Query changes let changes = self @@ -54,7 +54,7 @@ impl JMAP { _ => return Err(MethodError::UnknownMethod("Unknown method".to_string())), }, }, - acl_token, + access_token, ) .await?; let calculate_total = request.calculate_total.unwrap_or(false); @@ -87,11 +87,11 @@ impl JMAP { .map_or(false, |sort| sort.iter().any(|s| !s.is_immutable())); let results = match request.arguments { query::RequestArguments::Email(arguments) => { - self.email_query(query.with_arguments(arguments), acl_token) + self.email_query(query.with_arguments(arguments), access_token) .await? } query::RequestArguments::Mailbox(arguments) => { - self.mailbox_query(query.with_arguments(arguments), acl_token) + self.mailbox_query(query.with_arguments(arguments), access_token) .await? } query::RequestArguments::EmailSubmission => { diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index f3266dd9..aa06d7b5 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -54,7 +54,7 @@ use store::{ }; use utils::map::vec_map::VecMap; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; use super::{ index::{EmailIndexBuilder, TrimTextValue, MAX_SORT_FIELD_LENGTH}, @@ -65,7 +65,7 @@ impl JMAP { pub async fn email_copy( &self, request: CopyRequest, - acl_token: &AclToken, + access_token: &AccessToken, next_call: &mut Option>, ) -> Result { let account_id = request.account_id.document_id(); @@ -90,11 +90,11 @@ impl JMAP { }; let from_message_ids = self - .owned_or_shared_messages(acl_token, from_account_id, Acl::ReadItems) + .owned_or_shared_messages(access_token, from_account_id, Acl::ReadItems) .await?; let mailbox_ids = self.mailbox_get_or_create(account_id).await?; - let can_add_mailbox_ids = if acl_token.is_shared(account_id) { - self.shared_documents(acl_token, account_id, Collection::Mailbox, Acl::AddItems) + let can_add_mailbox_ids = if access_token.is_shared(account_id) { + self.shared_documents(access_token, account_id, Collection::Mailbox, Acl::AddItems) .await? .into() } else { diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs index 7bd5e95c..962688ce 100644 --- a/crates/jmap/src/email/get.rs +++ b/crates/jmap/src/email/get.rs @@ -32,7 +32,7 @@ use jmap_proto::{ }; use mail_parser::Message; -use crate::{auth::AclToken, email::headers::HeaderToValue, JMAP}; +use crate::{auth::AccessToken, email::headers::HeaderToValue, JMAP}; use super::body::{ToBodyPart, TruncateBody}; @@ -40,7 +40,7 @@ impl JMAP { pub async fn email_get( &self, mut request: GetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let ids = request.unwrap_ids(self.config.get_max_objects)?; let properties = request.unwrap_properties(&[ @@ -90,7 +90,7 @@ impl JMAP { let account_id = request.account_id.document_id(); let message_ids = self - .owned_or_shared_messages(acl_token, account_id, Acl::ReadItems) + .owned_or_shared_messages(access_token, account_id, Acl::ReadItems) .await?; let ids = if let Some(ids) = ids { ids diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index 47a24001..6c7e9fda 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -38,13 +38,13 @@ use jmap_proto::{ }; use utils::map::vec_map::VecMap; -use crate::{auth::AclToken, IngestError, JMAP}; +use crate::{auth::AccessToken, IngestError, JMAP}; impl JMAP { pub async fn email_import( &self, request: ImportEmailRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { // Validate state let account_id = request.account_id.document_id(); @@ -53,8 +53,8 @@ impl JMAP { .await?; let valid_mailbox_ids = self.mailbox_get_or_create(account_id).await?; - let can_add_mailbox_ids = if acl_token.is_shared(account_id) { - self.shared_documents(acl_token, account_id, Collection::Mailbox, Acl::AddItems) + let can_add_mailbox_ids = if access_token.is_shared(account_id) { + self.shared_documents(access_token, account_id, Collection::Mailbox, Acl::AddItems) .await? .into() } else { @@ -112,7 +112,7 @@ impl JMAP { } // Fetch raw message to import - let raw_message = match self.blob_download(&email.blob_id, acl_token).await? { + let raw_message = match self.blob_download(&email.blob_id, access_token).await? { Some(raw_message) => raw_message, None => { response.not_created.append( diff --git a/crates/jmap/src/email/parse.rs b/crates/jmap/src/email/parse.rs index 166a3c06..1337ac80 100644 --- a/crates/jmap/src/email/parse.rs +++ b/crates/jmap/src/email/parse.rs @@ -32,7 +32,7 @@ use mail_parser::{ }; use utils::map::vec_map::VecMap; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; use super::{ body::{ToBodyPart, TruncateBody}, @@ -44,7 +44,7 @@ impl JMAP { pub async fn email_parse( &self, request: ParseEmailRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { if request.blob_ids.len() > self.config.mail_parse_max_items { return Err(MethodError::RequestTooLarge); @@ -101,7 +101,7 @@ impl JMAP { for blob_id in request.blob_ids { // Fetch raw message to parse - let raw_message = match self.blob_download(&blob_id, acl_token).await? { + let raw_message = match self.blob_download(&blob_id, access_token).await? { Some(raw_message) => raw_message, None => { response.not_found.push(blob_id); diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs index bcac928f..d3854a5f 100644 --- a/crates/jmap/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -35,13 +35,13 @@ use store::{ ValueKey, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn email_query( &self, mut request: QueryRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let account_id = request.account_id.document_id(); let mut filters = Vec::with_capacity(request.filter.len()); @@ -240,9 +240,9 @@ impl JMAP { } let mut result_set = self.filter(account_id, Collection::Email, filters).await?; - if acl_token.is_shared(account_id) { + if access_token.is_shared(account_id) { result_set.apply_mask( - self.shared_messages(acl_token, account_id, Acl::ReadItems) + self.shared_messages(access_token, account_id, Acl::ReadItems) .await?, ); } diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 39370eb1..349ea5b9 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -60,7 +60,7 @@ use store::{ BlobKind, Serialize, ValueKey, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; use super::{ headers::{BuildHeader, ValueToHeader}, @@ -71,7 +71,7 @@ impl JMAP { pub async fn email_set( &self, mut request: SetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { // Prepare response let account_id = request.account_id.document_id(); @@ -81,17 +81,22 @@ impl JMAP { // Obtain mailboxIds let mailbox_ids = self.mailbox_get_or_create(account_id).await?; - let (can_add_mailbox_ids, can_delete_mailbox_ids, can_modify_message_ids) = if acl_token + let (can_add_mailbox_ids, can_delete_mailbox_ids, can_modify_message_ids) = if access_token .is_shared(account_id) { ( - self.shared_documents(acl_token, account_id, Collection::Mailbox, Acl::AddItems) + self.shared_documents(access_token, account_id, Collection::Mailbox, Acl::AddItems) .await? .into(), - self.shared_documents(acl_token, account_id, Collection::Mailbox, Acl::RemoveItems) - .await? - .into(), - self.shared_messages(acl_token, account_id, Acl::ModifyItems) + self.shared_documents( + access_token, + account_id, + Collection::Mailbox, + Acl::RemoveItems, + ) + .await? + .into(), + self.shared_messages(access_token, account_id, Acl::ModifyItems) .await? .into(), ) @@ -544,7 +549,7 @@ impl JMAP { headers, contents: if !is_multipart { if let Some(blob_id) = blob_id { - match self.blob_download(&blob_id, acl_token).await? { + match self.blob_download(&blob_id, access_token).await? { Some(contents) => { BodyPart::Binary(contents.into()) } @@ -957,8 +962,8 @@ impl JMAP { .get_document_ids(account_id, Collection::Email) .await? .unwrap_or_default(); - let can_destroy_message_ids = if acl_token.is_shared(account_id) { - self.shared_messages(acl_token, account_id, Acl::RemoveItems) + let can_destroy_message_ids = if access_token.is_shared(account_id) { + self.shared_messages(access_token, account_id, Acl::RemoveItems) .await? .into() } else { diff --git a/crates/jmap/src/email/snippet.rs b/crates/jmap/src/email/snippet.rs index 540be7c9..e325bdc4 100644 --- a/crates/jmap/src/email/snippet.rs +++ b/crates/jmap/src/email/snippet.rs @@ -42,7 +42,7 @@ use store::{ BlobKind, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; use super::index::MAX_MESSAGE_PARTS; @@ -50,7 +50,7 @@ impl JMAP { pub async fn email_search_snippet( &self, request: GetSearchSnippetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let mut filter_stack = vec![]; let mut include_term = true; @@ -102,7 +102,7 @@ impl JMAP { } let account_id = request.account_id.document_id(); let document_ids = self - .owned_or_shared_messages(acl_token, account_id, Acl::ReadItems) + .owned_or_shared_messages(access_token, account_id, Acl::ReadItems) .await?; let email_ids = request.email_ids.unwrap(); let mut response = GetSearchSnippetResponse { diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 70906133..cc9c0726 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -72,7 +72,13 @@ impl JMAP { // Validate email address if let Value::Text(email) = identity.get(&Property::Email) { - if !self.get_addresses_by_uid(account_id).await.contains(email) { + if !self + .directory + .emails_by_id(account_id) + .await + .unwrap_or_default() + .contains(email) + { response.not_created.append( id, SetError::invalid_properties() diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index aa463aba..aa5d3057 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -28,8 +28,9 @@ use api::session::BaseCapabilities; use auth::{ oauth::OAuthCode, rate_limit::{AnonymousLimiter, AuthenticatedLimiter, RemoteAddress}, - AclToken, + AccessToken, }; +use directory::{Directory, DirectoryConfig}; use jmap_proto::{ error::method::MethodError, method::{ @@ -77,9 +78,10 @@ pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24); pub struct JMAP { pub store: Store, pub config: Config, + pub directory: Arc, pub sessions: LruCache, - pub acl_tokens: LruCache>, + pub access_tokens: LruCache>, pub rate_limit_auth: LruCache>>, pub rate_limit_unauth: LruCache>>, @@ -154,108 +156,29 @@ pub enum IngestError { impl JMAP { pub async fn init( config: &utils::config::Config, + directory_config: &DirectoryConfig, delivery_rx: mpsc::Receiver, smtp: Arc, ) -> Result, String> { - let remove = "true"; - /* - let auth_db = match config.value_require("jmap.auth.database.type")? { - "ldap" => AuthDatabase::Ldap, - "sql" => { - let address = config.value_require("jmap.auth.database.address")?; - let max_connections = config - .property("jmap.auth.database.max-connections")? - .unwrap_or(10); - let min_connections = config - .property("jmap.auth.database.min-connections")? - .unwrap_or(0); - let idle_timeout = config.property("jmap.auth.database.idle-timeout")?; - - let db = if address.starts_with("postgres:") { - SqlDatabase::Postgres( - PgPoolOptions::new() - .max_connections(max_connections) - .min_connections(min_connections) - .idle_timeout(idle_timeout) - .connect_lazy(address) - .failed(&format!("Failed to create connection pool for {address:?}")), - ) - } else if address.starts_with("mysql:") { - SqlDatabase::MySql( - MySqlPoolOptions::new() - .max_connections(max_connections) - .min_connections(min_connections) - .idle_timeout(idle_timeout) - .connect_lazy(address) - .failed(&format!("Failed to create connection pool for {address:?}")), - ) - } else if address.starts_with("mssql:") { - todo!() - /*SqlDatabase::MsSql( - MssqlPoolOptions::new() - .max_connections(max_connections) - .min_connections(min_connections) - .idle_timeout(idle_timeout) - .connect_lazy(address) - .failed(&format!("Failed to create connection pool for {address:?}")), - )*/ - } else if address.starts_with("sqlite:") { - SqlDatabase::SqlLite( - SqlitePoolOptions::new() - .max_connections(max_connections) - .min_connections(min_connections) - .idle_timeout(idle_timeout) - .connect_lazy(address) - .failed(&format!("Failed to create connection pool for {address:?}")), - ) - } else { - failed(&format!("Invalid database address {address:?}")); - }; - AuthDatabase::Sql { - db, - query_uid_by_login: config - .value_require("jmap.auth.database.query.uid-by-login")? - .to_string(), - query_login_by_uid: config - .value_require("jmap.auth.database.query.login-by-uid")? - .to_string(), - query_secret_by_uid: config - .value_require("jmap.auth.database.query.secret-by-uid")? - .to_string(), - query_name_by_uid: config - .value_require("jmap.auth.database.query.name-by-uid")? - .to_string(), - query_gids_by_uid: config - .value_require("jmap.auth.database.query.gids-by-uid")? - .to_string(), - query_uids_by_address: config - .value_require("jmap.auth.database.query.uids-by-address")? - .to_string(), - query_addresses_by_uid: config - .value_require("jmap.auth.database.query.addresses-by-uid")? - .to_string(), - query_vrfy: config - .value_require("jmap.auth.database.query.vrfy")? - .to_string(), - query_expn: config - .value_require("jmap.auth.database.query.expn")? - .to_string(), - } - } - _ => failed("Invalid auth database type"), - };*/ - // Init state manager and housekeeper let (state_tx, state_rx) = init_state_manager(); let (housekeeper_tx, housekeeper_rx) = init_housekeeper(); let jmap_server = Arc::new(JMAP { + directory: directory_config + .directories + .get(config.value_require("jmap.directory")?) + .failed(&format!( + "Unable to find directory '{}'", + config.value_require("jmap.directory")? + )) + .clone(), store: Store::open(config).await.failed("Unable to open database"), config: Config::new(config).failed("Invalid configuration file"), sessions: LruCache::with_capacity( config.property("jmap.session.cache.size")?.unwrap_or(100), ), - acl_tokens: LruCache::with_capacity( + access_tokens: LruCache::with_capacity( config.property("jmap.session.cache.size")?.unwrap_or(100), ), rate_limit_auth: LruCache::with_capacity( diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs index db7937cb..885cd8ca 100644 --- a/crates/jmap/src/mailbox/get.rs +++ b/crates/jmap/src/mailbox/get.rs @@ -30,7 +30,7 @@ use jmap_proto::{ use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap}; use crate::{ - auth::{acl::EffectiveAcl, AclToken}, + auth::{acl::EffectiveAcl, AccessToken}, JMAP, }; @@ -38,7 +38,7 @@ impl JMAP { pub async fn mailbox_get( &self, mut request: GetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let ids = request.unwrap_ids(self.config.get_max_objects)?; let properties = request.unwrap_properties(&[ @@ -56,9 +56,9 @@ impl JMAP { ]); let account_id = request.account_id.document_id(); let mut mailbox_ids = self.mailbox_get_or_create(account_id).await?; - if acl_token.is_shared(account_id) { + if access_token.is_shared(account_id) { mailbox_ids &= self - .shared_documents(acl_token, account_id, Collection::Mailbox, Acl::Read) + .shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read) .await?; } let message_ids = self.get_document_ids(account_id, Collection::Email).await?; @@ -179,8 +179,8 @@ impl JMAP { .await? as u64, ), Property::MyRights => { - if acl_token.is_shared(account_id) { - let acl = values.effective_acl(acl_token); + if access_token.is_shared(account_id) { + let acl = values.effective_acl(access_token); Object::with_capacity(9) .with_property(Property::MayReadItems, acl.contains(Acl::ReadItems)) .with_property(Property::MayAddItems, acl.contains(Acl::AddItems)) @@ -220,7 +220,8 @@ impl JMAP { .remove(property) .map(|parent_id| match parent_id { Value::List(values) - if values.contains(&Value::Id(acl_token.primary_id().into())) => + if values + .contains(&Value::Id(access_token.primary_id().into())) => { Value::Bool(true) } @@ -235,7 +236,7 @@ impl JMAP { .and_then(|v| v.as_list()) .map(|v| &v[..]) .unwrap_or_else(|| &[]), - acl_token, + access_token, account_id, ) .await diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 59d5c986..00176a63 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -34,13 +34,13 @@ use store::{ roaring::RoaringBitmap, }; -use crate::{auth::AclToken, UpdateResults, JMAP}; +use crate::{auth::AccessToken, UpdateResults, JMAP}; impl JMAP { pub async fn mailbox_query( &self, mut request: QueryRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let account_id = request.account_id.document_id(); let sort_as_tree = request.arguments.sort_as_tree.unwrap_or(false); @@ -92,7 +92,7 @@ impl JMAP { } filters.push(query::Filter::eq( Property::IsSubscribed, - acl_token.primary_id, + access_token.primary_id, )); if !is_subscribed { filters.push(query::Filter::End); @@ -109,9 +109,9 @@ impl JMAP { let mut result_set = self .filter(account_id, Collection::Mailbox, filters) .await?; - if acl_token.is_shared(account_id) { + if access_token.is_shared(account_id) { result_set.apply_mask( - self.shared_documents(acl_token, account_id, Collection::Mailbox, Acl::Read) + self.shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read) .await?, ); } diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 6b04b2b9..0c7ba1fc 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -50,7 +50,7 @@ use store::{ }; use crate::{ - auth::{acl::EffectiveAcl, AclToken}, + auth::{acl::EffectiveAcl, AccessToken}, JMAP, SUPERUSER_ID, }; @@ -58,7 +58,7 @@ use super::{INBOX_ID, TRASH_ID}; struct SetContext<'x> { account_id: u32, - acl_token: &'x AclToken, + access_token: &'x AccessToken, is_shared: bool, response: SetResponse, mailbox_ids: RoaringBitmap, @@ -88,15 +88,15 @@ impl JMAP { pub async fn mailbox_set( &self, mut request: SetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { // Prepare response let account_id = request.account_id.document_id(); let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false); let mut ctx = SetContext { account_id, - is_shared: acl_token.is_shared(account_id), - acl_token, + is_shared: access_token.is_shared(account_id), + access_token, response: self .prepare_set_response(&request, Collection::Mailbox) .await?, @@ -153,7 +153,7 @@ impl JMAP { { // Validate ACL if ctx.is_shared { - let acl = mailbox.inner.effective_acl(acl_token); + let acl = mailbox.inner.effective_acl(access_token); if !acl.contains(Acl::Modify) { ctx.response.not_updated.append( id, @@ -223,8 +223,7 @@ impl JMAP { 'destroy: for id in ctx.will_destroy { let document_id = id.document_id(); // Internal folders cannot be deleted - if (document_id == INBOX_ID || document_id == TRASH_ID) - && !acl_token.is_member(SUPERUSER_ID) + if (document_id == INBOX_ID || document_id == TRASH_ID) && !access_token.is_super_user() { ctx.response.not_destroyed.append( id, @@ -388,7 +387,7 @@ impl JMAP { { // Validate ACLs if ctx.is_shared { - let acl = mailbox.inner.effective_acl(acl_token); + let acl = mailbox.inner.effective_acl(access_token); if !acl.contains(Acl::Administer) { if !acl.contains(Acl::Delete) { ctx.response.not_destroyed.append( @@ -513,7 +512,7 @@ impl JMAP { } (Property::ParentId, MaybePatchValue::Value(Value::Null)) => Value::Id(0u64.into()), (Property::IsSubscribed, MaybePatchValue::Value(Value::Bool(subscribe))) => { - let account_id = Value::Id(ctx.acl_token.primary_id().into()); + let account_id = Value::Id(ctx.access_token.primary_id().into()); let mut new_value = None; if let Some((_, current_fields)) = update.as_ref() { if let Value::List(subscriptions) = @@ -628,7 +627,7 @@ impl JMAP { if depth == 0 && ctx.is_shared && !fields - .effective_acl(ctx.acl_token) + .effective_acl(ctx.access_token) .contains_any([Acl::CreateChild, Acl::Administer].into_iter()) { return Ok(Err(SetError::forbidden().with_description( diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs index f1b55621..7d658ef4 100644 --- a/crates/jmap/src/push/get.rs +++ b/crates/jmap/src/push/get.rs @@ -31,7 +31,7 @@ use jmap_proto::{ use store::{write::now, BitmapKey, ValueKey}; use utils::map::bitmap::Bitmap; -use crate::{auth::AclToken, services::state, JMAP}; +use crate::{auth::AccessToken, services::state, JMAP}; use super::{EncryptionKeys, PushSubscription, UpdateSubscription}; @@ -39,7 +39,7 @@ impl JMAP { pub async fn push_subscription_get( &self, mut request: GetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let ids = request.unwrap_ids(self.config.get_max_objects)?; let properties = request.unwrap_properties(&[ @@ -49,7 +49,7 @@ impl JMAP { Property::Expires, Property::Types, ]); - let account_id = acl_token.primary_id(); + let account_id = access_token.primary_id(); let push_ids = self .get_document_ids(account_id, Collection::PushSubscription) .await? diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs index 264e77f2..86a78d3c 100644 --- a/crates/jmap/src/push/set.rs +++ b/crates/jmap/src/push/set.rs @@ -40,7 +40,7 @@ use store::{ write::{now, BatchBuilder, F_CLEAR, F_VALUE}, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; const EXPIRES_MAX: i64 = 7 * 24 * 3600; // 7 days const VERIFICATION_CODE_LEN: usize = 32; @@ -49,9 +49,9 @@ impl JMAP { pub async fn push_subscription_set( &self, mut request: SetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { - let account_id = acl_token.primary_id(); + let account_id = access_token.primary_id(); let mut push_ids = self .get_document_ids(account_id, Collection::PushSubscription) .await? diff --git a/crates/jmap/src/services/delivery.rs b/crates/jmap/src/services/delivery.rs index 37d57dff..a0908444 100644 --- a/crates/jmap/src/services/delivery.rs +++ b/crates/jmap/src/services/delivery.rs @@ -23,9 +23,8 @@ use std::sync::Arc; -use mail_send::Credentials; use tokio::sync::mpsc; -use utils::ipc::{DeliveryEvent, Item}; +use utils::ipc::DeliveryEvent; use crate::JMAP; @@ -36,25 +35,6 @@ pub fn spawn_delivery_manager(core: Arc, mut delivery_rx: mpsc::Receiver { result_tx.send(core.deliver_message(message).await).ok(); } - DeliveryEvent::Lookup(lookup) => { - lookup - .result - .send(match lookup.item { - Item::IsAccount(address) => { - (!core.get_uids_by_address(&address).await.is_empty()).into() - } - Item::Authenticate(credentials) => match credentials { - Credentials::Plain { username, secret } => { - core.authenticate(&username, &secret).await.is_some() - } - _ => false, - } - .into(), - Item::Verify(address) => core.vrfy_address(&address).await.into(), - Item::Expand(address) => core.expn_address(&address).await.into(), - }) - .ok(); - } DeliveryEvent::Stop => break, } } diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 496571b0..620ae550 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -45,7 +45,7 @@ impl JMAP { let mut recipients = Vec::with_capacity(message.recipients.len()); let mut deliver_uids = AHashMap::with_capacity(message.recipients.len()); for rcpt in &message.recipients { - let uids = self.get_uids_by_address(rcpt).await; + let uids = self.directory.ids_by_email(rcpt).await.unwrap_or_default(); for uid in &uids { deliver_uids.insert(*uid, (DeliveryResult::Success, rcpt)); } diff --git a/crates/jmap/src/services/state.rs b/crates/jmap/src/services/state.rs index 6eaa19f6..9c810183 100644 --- a/crates/jmap/src/services/state.rs +++ b/crates/jmap/src/services/state.rs @@ -114,7 +114,7 @@ pub fn spawn_state_manager( } Event::UpdateSharedAccounts { account_id } => { // Obtain account membership and shared mailboxes - let acl = match core.get_acl_token(account_id).await { + let acl = match core.get_access_token(account_id).await { Some(result) => result, None => { continue; diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index f5c8de2b..3a569ab3 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -78,8 +78,10 @@ impl JMAP { // Obtain mail from address let mail_from = if let Some(email) = self - .get_addresses_by_uid(account_id) + .directory + .emails_by_id(account_id) .await + .unwrap_or_default() .into_iter() .next() { @@ -92,8 +94,8 @@ impl JMAP { instance.set_user_address(&mail_from); // Set account name - if let Some(name) = self.get_account_name(account_id).await { - instance.set_user_full_name(&name); + if let Ok(Some(p)) = self.directory.principal_by_id(account_id).await { + instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); } // Set envelope diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index a72d8956..80a5c55d 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -50,11 +50,11 @@ use store::{ BlobKind, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; struct SetContext<'x> { account_id: u32, - acl_token: &'x AclToken, + access_token: &'x AccessToken, response: SetResponse, } @@ -73,7 +73,7 @@ impl JMAP { pub async fn sieve_script_set( &self, mut request: SetRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { let account_id = request.account_id.document_id(); let mut sieve_ids = self @@ -82,7 +82,7 @@ impl JMAP { .unwrap_or_default(); let mut ctx = SetContext { account_id, - acl_token, + access_token, response: self .prepare_set_response(&request, Collection::SieveScript) .await?, @@ -450,7 +450,7 @@ impl JMAP { .is_document(ctx.account_id, Collection::SieveScript, *document_id) }) { // Check access - if let Some(mut bytes) = self.blob_download(&blob_id, ctx.acl_token).await? { + if let Some(mut bytes) = self.blob_download(&blob_id, ctx.access_token).await? { // Compile script match self.sieve_compiler.compile(&bytes) { Ok(script) => { diff --git a/crates/jmap/src/sieve/validate.rs b/crates/jmap/src/sieve/validate.rs index a75c36a3..e28f24bd 100644 --- a/crates/jmap/src/sieve/validate.rs +++ b/crates/jmap/src/sieve/validate.rs @@ -29,18 +29,18 @@ use jmap_proto::{ method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse}, }; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn sieve_script_validate( &self, request: ValidateSieveScriptRequest, - acl_token: &AclToken, + access_token: &AccessToken, ) -> Result { Ok(ValidateSieveScriptResponse { account_id: request.account_id, error: match self - .blob_download(&request.blob_id, acl_token) + .blob_download(&request.blob_id, access_token) .await? .map(|bytes| self.sieve_compiler.compile(&bytes)) { diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs index a0b94b9e..f3708861 100644 --- a/crates/jmap/src/websocket/stream.rs +++ b/crates/jmap/src/websocket/stream.rs @@ -36,18 +36,18 @@ use tokio_tungstenite::WebSocketStream; use tungstenite::Message; use utils::{listener::ServerInstance, map::bitmap::Bitmap}; -use crate::{auth::AclToken, JMAP}; +use crate::{auth::AccessToken, JMAP}; impl JMAP { pub async fn handle_websocket_stream( &self, mut stream: WebSocketStream, - acl_token: Arc, + access_token: Arc, instance: Arc, ) { let span = tracing::info_span!( "WebSocket connection established", - "account_id" = acl_token.primary_id(), + "account_id" = access_token.primary_id(), "url" = instance.data, ); @@ -63,8 +63,8 @@ impl JMAP { // Register with state manager let mut change_rx = if let Some(change_rx) = self .subscribe_state_manager( - acl_token.primary_id(), - acl_token.primary_id(), + access_token.primary_id(), + access_token.primary_id(), Bitmap::all(), ) .await @@ -97,7 +97,7 @@ impl JMAP { match self .handle_request( request.request, - acl_token.clone(), + access_token.clone(), &instance, ) .await diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs index 6c616b71..1c3ff98e 100644 --- a/crates/jmap/src/websocket/upgrade.rs +++ b/crates/jmap/src/websocket/upgrade.rs @@ -32,14 +32,14 @@ use utils::listener::ServerInstance; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse}, - auth::AclToken, + auth::AccessToken, JMAP, }; pub async fn upgrade_websocket_connection( jmap: Arc, req: HttpRequest, - acl_token: Arc, + access_token: Arc, instance: Arc, ) -> HttpResponse { let headers = req.headers(); @@ -85,7 +85,7 @@ pub async fn upgrade_websocket_connection( Ok(upgraded) => { jmap.handle_websocket_stream( WebSocketStream::from_raw_socket(upgraded, Role::Server, None).await, - acl_token, + access_token, instance, ) .await; diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 73c0a475..0187c536 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -53,7 +53,7 @@ async fn main() -> std::io::Result<()> { let smtp = SMTP::init(&config, &servers, &directory, delivery_tx) .await .failed("Invalid configuration file"); - let jmap = JMAP::init(&config, delivery_rx, smtp.clone()) + let jmap = JMAP::init(&config, &directory, delivery_rx, smtp.clone()) .await .failed("Invalid configuration file"); diff --git a/crates/utils/src/ipc.rs b/crates/utils/src/ipc.rs index 0e12395e..774776af 100644 --- a/crates/utils/src/ipc.rs +++ b/crates/utils/src/ipc.rs @@ -23,7 +23,6 @@ use std::{borrow::Cow, path::PathBuf}; -use mail_send::Credentials; use tokio::{fs, io::AsyncReadExt, sync::oneshot}; #[derive(Debug)] @@ -32,7 +31,6 @@ pub enum DeliveryEvent { message: IngestMessage, result_tx: oneshot::Sender>, }, - Lookup(LookupItem), Stop, } @@ -56,64 +54,6 @@ pub enum DeliveryResult { }, } -#[derive(Clone, PartialEq, Eq, Hash)] -pub enum Item { - IsAccount(String), - Authenticate(Credentials), - Verify(String), - Expand(String), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LookupResult { - True, - False, - Values(Vec), -} - -#[derive(Debug)] -pub struct LookupItem { - pub item: Item, - pub result: oneshot::Sender, -} - -impl From for bool { - fn from(value: LookupResult) -> Self { - matches!(value, LookupResult::True | LookupResult::Values(_)) - } -} - -impl From for LookupResult { - fn from(value: bool) -> Self { - if value { - LookupResult::True - } else { - LookupResult::False - } - } -} - -impl From> for LookupResult { - fn from(value: Vec) -> Self { - if !value.is_empty() { - LookupResult::Values(value) - } else { - LookupResult::False - } - } -} - -impl core::fmt::Debug for Item { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::IsAccount(arg0) => f.debug_tuple("Rcpt").field(arg0).finish(), - Self::Authenticate(_) => f.debug_tuple("Auth").finish(), - Self::Expand(arg0) => f.debug_tuple("Expn").field(arg0).finish(), - Self::Verify(arg0) => f.debug_tuple("Vrfy").field(arg0).finish(), - } - } -} - impl IngestMessage { pub async fn read_message(&self) -> Result, ()> { let mut raw_message = vec![0u8; self.message_size]; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 2ca329b8..b3ec8e64 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -12,7 +12,7 @@ foundationdb = ["store/foundation"] [dev-dependencies] store = { path = "../crates/store", features = ["test_mode"] } directory = { path = "../crates/directory" } -#jmap = { path = "../crates/jmap", features = ["test_mode"] } +jmap = { path = "../crates/jmap", features = ["test_mode"] } jmap_proto = { path = "../crates/jmap-proto" } smtp = { path = "../crates/smtp", features = ["test_mode", "local_delivery"] } smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" } diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 4df5b814..6760f1e6 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -12,7 +12,7 @@ use tokio_rustls::TlsAcceptor; const CONFIG: &str = r#" [directory."sql"] -protocol = "sql" +type = "sql" address = "sqlite::memory:" [directory."sql".pool] @@ -42,7 +42,7 @@ type = "type" domains = "SELECT name FROM domains WHERE name = ?" [directory."ldap"] -protocol = "ldap" +type = "ldap" address = "ldap://localhost:3893" base-dn = "dc=example,dc=org" @@ -77,7 +77,7 @@ email-alias = "givenName" quota = "diskQuota" [directory."imap"] -protocol = "imap" +type = "imap" address = "127.0.0.1" port = 9198 @@ -89,7 +89,7 @@ implicit = true allow-invalid-certs = true [directory."smtp"] -protocol = "lmtp" +type = "lmtp" address = "127.0.0.1" port = 9199 @@ -109,7 +109,7 @@ entries = 500 ttl = {positive = '10s', negative = '5s'} [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index bd5489c6..f172b6d4 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -293,16 +293,22 @@ pub async fn set_test_quota(handle: &dyn Directory, login: &str, quota: u32) { } pub async fn add_to_group(handle: &dyn Directory, login: &str, group: &str) { - let user = handle.principal_by_name(login).await.unwrap().unwrap(); let group = handle.principal_by_name(group).await.unwrap().unwrap(); - - let uid = user.id; let gid = group.id; + assert_ne!(gid, u32::MAX, "{group:?}"); - assert_ne!(uid, gid, "{user:?} {group:?}"); - assert_ne!(uid, u32::MAX, "{user:?} {group:?}"); - assert_ne!(gid, u32::MAX, "{user:?} {group:?}"); + add_to_group_id(handle, login, gid).await; +} +pub async fn add_to_group_id(handle: &dyn Directory, login: &str, gid: u32) { + let user = handle.principal_by_name(login).await.unwrap().unwrap(); + let uid = user.id; + assert_ne!(uid, u32::MAX, "{user:?}"); + assert_ne!(uid, gid, "{user:?}"); + add_user_id_to_group_id(handle, uid, gid).await; +} + +pub async fn add_user_id_to_group_id(handle: &dyn Directory, uid: u32, gid: u32) { handle .query( &format!( @@ -318,7 +324,10 @@ pub async fn add_to_group(handle: &dyn Directory, login: &str, group: &str) { pub async fn remove_from_group(handle: &dyn Directory, uid: u32, gid: u32) { handle .query( - &format!("DELETE FROM groups WHERE uid = {} AND gid = {}", uid, gid), + &format!( + "DELETE FROM group_members WHERE uid = {} AND gid = {}", + uid, gid + ), &[], ) .await diff --git a/tests/src/jmap/auth_acl.rs b/tests/src/jmap/auth_acl.rs index 5e06673e..54fe200d 100644 --- a/tests/src/jmap/auth_acl.rs +++ b/tests/src/jmap/auth_acl.rs @@ -38,8 +38,17 @@ use jmap_client::{ principal::ACL, }; use jmap_proto::types::id::Id; +use std::fmt::Debug; use store::ahash::AHashMap; +use crate::{ + directory::sql::{ + add_user_id_to_group_id, create_test_group_with_email, create_test_user_with_email, + remove_from_group, + }, + jmap::{mailbox::destroy_all_mailboxes, test_account_login}, +}; + pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running ACL tests..."); @@ -47,11 +56,16 @@ pub async fn test(server: Arc, admin_client: &mut Client) { let inbox_id = Id::new(INBOX_ID as u64).to_string(); let trash_id = Id::new(TRASH_ID as u64).to_string(); - let john_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe").await; + let directory = server.directory.as_ref(); + let john_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; let jane_id = - test_account_create(&server, "jane.smith@example.com", "abcde", "Jane Smith").await; - let bill_id = test_account_create(&server, "bill@example.com", "098765", "Bill Foobar").await; - let sales_id = test_account_create(&server, "sales@example.com", "", "Sales Group").await; + create_test_user_with_email(directory, "jane.smith@example.com", "abcde", "Jane Smith") + .await; + let bill_id = + create_test_user_with_email(directory, "bill@example.com", "098765", "Bill Foobar").await; + let sales_id = + create_test_group_with_email(directory, "sales@example.com", "Sales Group").await; // Authenticate all accounts let mut john_client = test_account_login("jdoe@example.com", "12345").await; @@ -639,21 +653,9 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Add John and Jane to the Sales group for id in [jane_id.id(), john_id.id()] { - assert!( - server - .auth_db - .execute( - &format!( - "INSERT INTO groups (uid, gid) VALUES ({}, {})", - id, - sales_id.id() - ), - Vec::::new().into_iter(), - ) - .await - ); + add_user_id_to_group_id(directory, id as u32, sales_id.id() as u32).await; } - server.acl_tokens.lock().clear(); + server.access_tokens.lock().clear(); john_client.refresh_session().await.unwrap(); jane_client.refresh_session().await.unwrap(); bill_client.refresh_session().await.unwrap(); @@ -747,19 +749,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { ); // Remove John from the sales group - assert!( - server - .auth_db - .execute( - &format!( - "DELETE FROM groups WHERE uid = {} AND gid ={}", - john_id.id(), - sales_id.id() - ), - Vec::::new().into_iter(), - ) - .await - ); + remove_from_group(directory, john_id.id() as u32, sales_id.id() as u32).await; server.sessions.lock().clear(); assert_forbidden( john_client @@ -776,9 +766,6 @@ pub async fn test(server: Arc, admin_client: &mut Client) { server.store.assert_is_empty().await; } -use std::fmt::Debug; - -use crate::jmap::{mailbox::destroy_all_mailboxes, test_account_create, test_account_login}; pub fn assert_forbidden(result: Result) { if !matches!( result, diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 8bc47457..4216c419 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -30,16 +30,27 @@ use jmap_client::{ mailbox::{self}, }; -use crate::jmap::{mailbox::destroy_all_mailboxes, test_account_create, test_alias_create}; +use crate::{ + directory::sql::{create_test_user_with_email, link_test_address}, + jmap::mailbox::destroy_all_mailboxes, +}; pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running Authorization tests..."); // Create test account - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); - test_alias_create(&server, "jdoe@example.com", "john.doe@example.com", false).await; + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); + link_test_address( + directory, + "jdoe@example.com", + "john.doe@example.com", + "alias", + ) + .await; // Reset rate limiters server.rate_limit_auth.lock().clear(); @@ -98,7 +109,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { assert_eq!(client.session().username(), "jdoe@example.com"); assert_eq!( client.session().account(&account_id).unwrap().name(), - "jdoe@example.com" + "John Doe" ); assert!(client.session().account(&account_id).unwrap().is_personal()); diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index 8ea1cd7d..c15355cf 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -36,13 +36,14 @@ use reqwest::{header, redirect::Policy}; use serde::de::DeserializeOwned; use store::ahash::AHashMap; -use crate::jmap::{mailbox::destroy_all_mailboxes, test_account_create}; +use crate::{directory::sql::create_test_user_with_email, jmap::mailbox::destroy_all_mailboxes}; pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running OAuth tests..."); // Create test account - let john_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") + let directory = server.directory.as_ref(); + let john_id = create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") .await .to_string(); diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index b3ecc0db..1790d268 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -31,29 +31,40 @@ use tokio::{ net::TcpStream, }; -use crate::jmap::{ - mailbox::destroy_all_mailboxes, test_account_create, test_alias_create, test_alias_remove, +use crate::{ + directory::sql::{create_test_user_with_email, link_test_address, remove_test_alias}, + jmap::mailbox::destroy_all_mailboxes, }; pub async fn test(server: Arc, client: &mut Client) { println!("Running message delivery tests..."); // Create a domain name and a test account - let account_id_1 = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); - let account_id_2 = test_account_create(&server, "jane@example.com", "abcdef", "Jane Smith") - .await - .to_string(); - let account_id_3 = test_account_create(&server, "bill@example.com", "098765", "Bill Foobar") - .await - .to_string(); - test_alias_create(&server, "jdoe@example.com", "john.doe@example.com", false).await; + let directory = server.directory.as_ref(); + let account_id_1 = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); + let account_id_2 = + create_test_user_with_email(directory, "jane@example.com", "abcdef", "Jane Smith") + .await + .to_string(); + let account_id_3 = + create_test_user_with_email(directory, "bill@example.com", "098765", "Bill Foobar") + .await + .to_string(); + link_test_address( + directory, + "jdoe@example.com", + "john.doe@example.com", + "alias", + ) + .await; // Create a mailing list - test_alias_create(&server, "jdoe@example.com", "members@example.com", true).await; - test_alias_create(&server, "jane@example.com", "members@example.com", true).await; - test_alias_create(&server, "bill@example.com", "members@example.com", true).await; + link_test_address(directory, "jdoe@example.com", "members@example.com", "list").await; + link_test_address(directory, "jane@example.com", "members@example.com", "list").await; + link_test_address(directory, "bill@example.com", "members@example.com", "list").await; // Delivering to individuals let mut lmtp = SmtpConnection::connect().await; @@ -162,7 +173,7 @@ pub async fn test(server: Arc, client: &mut Client) { } // Removing members from the mailing list and chunked ingest - test_alias_remove(&server, "jdoe@example.com", "members@example.com").await; + remove_test_alias(directory, "jdoe@example.com", "members@example.com").await; lmtp.ingest_chunked( "bill@example.com", &["members@example.com"], diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index e3b46417..eb79be52 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -44,8 +44,9 @@ use tokio::{ sync::mpsc, }; -use crate::jmap::{ - email_set::assert_email_properties, mailbox::destroy_all_mailboxes, test_account_create, +use crate::{ + directory::sql::create_test_user_with_email, + jmap::{email_set::assert_email_properties, mailbox::destroy_all_mailboxes}, }; #[derive(Default, Debug, PartialEq, Eq)] @@ -89,9 +90,11 @@ pub async fn test(server: Arc, client: &mut Client) { ); // Create a test account - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); // Create an identity without using a valid address should fail match client diff --git a/tests/src/jmap/event_source.rs b/tests/src/jmap/event_source.rs index 5691cc18..6b16ddc8 100644 --- a/tests/src/jmap/event_source.rs +++ b/tests/src/jmap/event_source.rs @@ -30,18 +30,20 @@ use jmap_proto::types::id::Id; use store::ahash::AHashSet; use tokio::sync::mpsc; -use crate::jmap::{ - delivery::SmtpConnection, mailbox::destroy_all_mailboxes, test_account_create, - test_account_login, +use crate::{ + directory::sql::create_test_user_with_email, + jmap::{delivery::SmtpConnection, mailbox::destroy_all_mailboxes, test_account_login}, }; pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running EventSource tests..."); // Create test account - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); let client = test_account_login("jdoe@example.com", "12345").await; let mut changes = client diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 206df97c..2255e70b 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -23,6 +23,7 @@ use std::{sync::Arc, time::Duration}; +use directory::config::ConfigDirectory; use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP}; use jmap_client::client::{Client, Credentials}; use jmap_proto::types::id::Id; @@ -30,7 +31,11 @@ use smtp::core::{SmtpSessionManager, SMTP}; use tokio::sync::{mpsc, watch}; use utils::{config::ServerProtocol, UnwrapFailure}; -use crate::{add_test_certs, store::TempDir}; +use crate::{ + add_test_certs, + directory::sql::{add_to_group_id, create_test_directory, create_test_user}, + store::TempDir, +}; pub mod auth_acl; pub mod auth_limits; @@ -85,21 +90,12 @@ reject-non-fqdn = false [session.rcpt] relay = [ { if = "authenticated-as", ne = "", then = true }, { else = false } ] - -[session.rcpt.lookup] -domains = "list/domains" -addresses = "local" -vrfy = "local" -expn = "local" +directory = "sql" [session.rcpt.errors] total = 5 wait = "1ms" -[list] -domains = ["example.com"] -remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"] - [queue] path = "{TMP}" hash = 64 @@ -112,8 +108,8 @@ hash = 64 type = "system" [queue.outbound] -next-hop = [ { if = "rcpt-domain", in-list = "list/domains", then = "local" }, - { if = "rcpt-domain", in-list = "list/remote-domains", then = "mock-smtp" }, +next-hop = [ { if = "rcpt-domain", in-list = "local/domains", then = "local" }, + { if = "rcpt-domain", in-list = "local/remote-domains", then = "mock-smtp" }, { else = false } ] [remote."mock-smtp"] @@ -137,6 +133,9 @@ blob.path = "{TMP}" cert = "file://{CERT}" private-key = "file://{PK}" +[jmap] +directory = "sql" + [jmap.protocol] set.max-objects = 100000 @@ -166,15 +165,19 @@ attempts.interval = "500ms" type = "sql" address = "sqlite::memory:" +[directory."sql".pool] +max-connections = 1 + [directory."sql".query] -login = "SELECT id, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" -name = "SELECT id, type, description, quota FROM accounts WHERE name = ?" -id = "SELECT name, type, description, quota FROM accounts WHERE id = ?" +login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" +name = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ?" +id = "SELECT id, name, type, secret, description, quota FROM accounts WHERE id = ?" members = "SELECT gid FROM group_members WHERE uid = ?" recipients = "SELECT id FROM emails WHERE address = ?" -emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC" -verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type != 'list' LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' LIMIT 50" +emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC" +verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" +expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" +domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" [directory."sql".columns] name = "name" @@ -185,36 +188,12 @@ email = "address" quota = "quota" type = "type" -[directory."ldap"] -type = "ldap" -address = "ldap://localhost:3893" -base-dn = "dc=example,dc=com" +[directory."local"] +type = "memory" -[directory."ldap".bind] -dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=com" -secret = "mysecret" - -[directory."ldap".filter] -login = "(&(objectClass=posixAccount)(accountStatus=active)(cn=?))" -name = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(cn=?))" -email = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(!(mail=?)(mailAliases=?)(mailLists=?)))" -id = "(|(&(objectClass=posixAccount)(uidNumber=?))(&(objectClass=posixGroup)(gidNumber=?)))" -verify = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(!(mail=*?*)(mailAliases=*?*)))" -expand = "(&(!(objectClass=posixAccount)(objectClass=posixGroup))(mailLists=?))" - -[directory."ldap".object-classes] -user = "posixAccount" -group = "posixGroup" - -[directory."ldap".attributes] -name = "cn" -description = "description" -secret = "userPassword" -groups = "memberOf" -id = ["uidNumber", "gidNumber"] -email = "mail" -email-alias = "mailAliases" -quota = "diskQuota" +[directory."local".lookup] +domains = ["example.com"] +remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"] [oauth] key = "parerga_und_paralipomena" @@ -282,14 +261,15 @@ async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest { ) .unwrap(); let servers = config.parse_servers().unwrap(); + let directory = config.parse_directory().unwrap(); // Start JMAP and SMTP servers servers.bind(&config); let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); - let smtp = SMTP::init(&config, &servers, delivery_tx) + let smtp = SMTP::init(&config, &servers, &directory, delivery_tx) .await .failed("Invalid configuration file"); - let jmap = JMAP::init(&config, delivery_rx, smtp.clone()) + let jmap = JMAP::init(&config, &directory, delivery_rx, smtp.clone()) .await .failed("Invalid configuration file"); let shutdown_tx = servers.spawn(|server, shutdown_rx| { @@ -305,19 +285,9 @@ async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest { }); // Create tables - for query in [ - "CREATE TABLE users (login TEXT PRIMARY KEY, secret TEXT, name TEXT)", - "CREATE TABLE groups (uid INTEGER, gid INTEGER, PRIMARY KEY (uid, gid))", - "CREATE TABLE emails (uid INTEGER NOT NULL, email TEXT NOT NULL, is_list BOOLEAN DEFAULT 0, PRIMARY KEY (uid, email))", - "INSERT INTO users (login, secret) VALUES ('admin', 'secret')", // RowID 0 is admin - ] { - assert!( - jmap.auth_db - .execute(query, Vec::::new().into_iter()) - .await, - "failed for {query}" - ); - } + create_test_directory(jmap.directory.as_ref()).await; + create_test_user(jmap.directory.as_ref(), "admin", "secret", "Superuser").await; + add_to_group_id(jmap.directory.as_ref(), "admin", 0).await; if delete_if_exists { jmap.store.destroy().await; @@ -397,58 +367,6 @@ pub fn replace_blob_ids(string: String) -> String { } } -pub async fn test_account_create(jmap: &JMAP, login: &str, secret: &str, name: &str) -> Id { - assert!( - jmap.auth_db - .execute( - "INSERT OR IGNORE INTO users (login, secret, name) VALUES (?, ?, ?)", - vec![login.to_string(), secret.to_string(), name.to_string()].into_iter() - ) - .await - ); - let uid = jmap.get_account_id(login).await.unwrap() as u64; - assert!( - jmap.auth_db - .execute( - &format!( - "INSERT OR IGNORE INTO emails (uid, email) VALUES ({}, ?)", - uid - ), - vec![login.to_string()].into_iter() - ) - .await - ); - Id::new(uid) -} - -pub async fn test_alias_create(jmap: &JMAP, login: &str, alias: &str, is_list: bool) { - let uid = jmap.get_account_id(login).await.unwrap() as u64; - assert!( - jmap.auth_db - .execute( - &format!( - "INSERT OR REPLACE INTO emails (uid, email, is_list) VALUES ({}, ?, {})", - uid, - if is_list { "true" } else { "false" } - ), - vec![alias.to_string()].into_iter() - ) - .await - ); -} - -pub async fn test_alias_remove(jmap: &JMAP, login: &str, alias: &str) { - let uid = jmap.get_account_id(login).await.unwrap() as u64; - assert!( - jmap.auth_db - .execute( - &format!("DELETE FROM emails WHERE uid = {} AND email = ?", uid), - vec![alias.to_string()].into_iter() - ) - .await - ); -} - pub async fn test_account_login(login: &str, secret: &str) -> Client { Client::new() .credentials(Credentials::basic(login, secret)) diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs index 4a784727..ee596e20 100644 --- a/tests/src/jmap/push_subscription.rs +++ b/tests/src/jmap/push_subscription.rs @@ -49,7 +49,8 @@ use utils::listener::SessionData; use crate::{ add_test_certs, - jmap::{mailbox::destroy_all_mailboxes, test_account_create, test_account_login}, + directory::sql::create_test_user_with_email, + jmap::{mailbox::destroy_all_mailboxes, test_account_login}, }; const SERVER: &str = " @@ -78,9 +79,11 @@ pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running Push Subscription tests..."); // Create test account - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe").await; + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await; admin_client.set_default_account_id(account_id); - let mut client = test_account_login("jdoe@example.com", "12345").await; + let client = test_account_login("jdoe@example.com", "12345").await; // Create channels let (event_tx, mut event_rx) = mpsc::channel::(100); @@ -127,13 +130,12 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Create a mailbox and expect a state change let mailbox_id = client - .set_default_account_id(Id::new(1).to_string()) .mailbox_create("PushSubscription Test", None::, Role::None) .await .unwrap() .take_id(); - assert_state(&mut event_rx, &[TypeState::Mailbox]).await; + assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await; // Receive states just for the requested types client @@ -187,14 +189,14 @@ pub async fn test(server: Arc, admin_client: &mut Client) { .unwrap(); tokio::time::sleep(Duration::from_millis(200)).await; push_server.fail_requests.store(false, Ordering::Relaxed); - assert_state(&mut event_rx, &[TypeState::Mailbox]).await; + assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await; // Make a mailbox change and expect state change client .mailbox_rename(&mailbox_id, "My Mailbox") .await .unwrap(); - assert_state(&mut event_rx, &[TypeState::Mailbox]).await; + assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await; //expect_nothing(&mut event_rx).await; // Multiple change updates should be grouped and pushed in intervals @@ -204,7 +206,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { .await .unwrap(); } - assert_state(&mut event_rx, &[TypeState::Mailbox]).await; + assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await; expect_nothing(&mut event_rx).await; // Destroy mailbox @@ -337,7 +339,10 @@ impl utils::listener::SessionManager for SessionManager { async fn expect_push(event_rx: &mut mpsc::Receiver) -> PushMessage { match tokio::time::timeout(Duration::from_millis(1500), event_rx.recv()).await { - Ok(Some(push)) => push, + Ok(Some(push)) => { + //println!("Push received: {:?}", push); + push + } result => { panic!("Timeout waiting for push: {:?}", result); } @@ -353,13 +358,13 @@ async fn expect_nothing(event_rx: &mut mpsc::Receiver) { } } -async fn assert_state(event_rx: &mut mpsc::Receiver, state: &[TypeState]) { +async fn assert_state(event_rx: &mut mpsc::Receiver, id: &Id, state: &[TypeState]) { assert_eq!( expect_push(event_rx) .await .unwrap_state_change() .changed - .get(&Id::new(1)) + .get(id) .unwrap() .iter() .map(|x| x.0) diff --git a/tests/src/jmap/sieve_script.rs b/tests/src/jmap/sieve_script.rs index 3567d3b7..386c6a45 100644 --- a/tests/src/jmap/sieve_script.rs +++ b/tests/src/jmap/sieve_script.rs @@ -37,20 +37,24 @@ use jmap_client::{ Error, }; -use crate::jmap::{ - delivery::SmtpConnection, - email_submission::{assert_message_delivery, spawn_mock_smtp_server, MockMessage}, - mailbox::destroy_all_mailboxes, - test_account_create, +use crate::{ + directory::sql::create_test_user_with_email, + jmap::{ + delivery::SmtpConnection, + email_submission::{assert_message_delivery, spawn_mock_smtp_server, MockMessage}, + mailbox::destroy_all_mailboxes, + }, }; pub async fn test(server: Arc, client: &mut Client) { println!("Running Sieve tests..."); // Create test account - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); client.set_default_account_id(&account_id); // Validate scripts diff --git a/tests/src/jmap/vacation_response.rs b/tests/src/jmap/vacation_response.rs index 3c03a3b0..478dae0b 100644 --- a/tests/src/jmap/vacation_response.rs +++ b/tests/src/jmap/vacation_response.rs @@ -27,22 +27,26 @@ use chrono::{Duration, Utc}; use jmap::JMAP; use jmap_client::client::Client; -use crate::jmap::{ - delivery::SmtpConnection, - email_submission::{ - assert_message_delivery, expect_nothing, spawn_mock_smtp_server, MockMessage, +use crate::{ + directory::sql::create_test_user_with_email, + jmap::{ + delivery::SmtpConnection, + email_submission::{ + assert_message_delivery, expect_nothing, spawn_mock_smtp_server, MockMessage, + }, + mailbox::destroy_all_mailboxes, }, - mailbox::destroy_all_mailboxes, - test_account_create, }; pub async fn test(server: Arc, client: &mut Client) { println!("Running Vacation Response tests..."); // Create test account - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); client.set_default_account_id(&account_id); // Start mock SMTP server diff --git a/tests/src/jmap/websocket.rs b/tests/src/jmap/websocket.rs index 0448fe34..0b311240 100644 --- a/tests/src/jmap/websocket.rs +++ b/tests/src/jmap/websocket.rs @@ -35,18 +35,22 @@ use jmap_client::{ }, TypeState, }; -use jmap_proto::types::id::Id; use tokio::sync::mpsc; -use crate::jmap::{mailbox::destroy_all_mailboxes, test_account_create, test_account_login}; +use crate::{ + directory::sql::create_test_user_with_email, + jmap::{mailbox::destroy_all_mailboxes, test_account_login}, +}; pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running WebSockets tests..."); // Authenticate all accounts - let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") - .await - .to_string(); + let directory = server.directory.as_ref(); + let account_id = + create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); let client = test_account_login("jdoe@example.com", "12345").await; let mut ws_stream = client.connect_ws().await.unwrap(); @@ -90,7 +94,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { .mailbox_update_sort_order(&mailbox_id, 1) .await .unwrap(); - assert_state(&mut stream_rx, &[TypeState::Mailbox]).await; + assert_state(&mut stream_rx, &account_id, &[TypeState::Mailbox]).await; // Multiple changes should be grouped and delivered in intervals for num in 0..5 { @@ -100,7 +104,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { .unwrap(); } tokio::time::sleep(Duration::from_millis(500)).await; - assert_state(&mut stream_rx, &[TypeState::Mailbox]).await; + assert_state(&mut stream_rx, &account_id, &[TypeState::Mailbox]).await; expect_nothing(&mut stream_rx).await; // Disable push notifications @@ -140,13 +144,17 @@ async fn expect_response( } } -async fn assert_state(stream_rx: &mut mpsc::Receiver, state: &[TypeState]) { +async fn assert_state( + stream_rx: &mut mpsc::Receiver, + id: &str, + state: &[TypeState], +) { match tokio::time::timeout(Duration::from_millis(700), stream_rx.recv()).await { Ok(Some(message)) => match message { WebSocketMessage::StateChange(changes) => { assert_eq!( changes - .changes(&Id::new(1).to_string()) + .changes(id) .unwrap() .map(|x| x.0) .collect::>(), diff --git a/tests/src/lib.rs b/tests/src/lib.rs index f9c7c1b9..f095c462 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -25,12 +25,12 @@ use std::path::PathBuf; #[cfg(test)] pub mod directory; -//#[cfg(test)] -//pub mod jmap; +#[cfg(test)] +pub mod jmap; #[cfg(test)] pub mod smtp; -//#[cfg(test)] -//pub mod store; +#[cfg(test)] +pub mod store; pub fn add_test_certs(config: &str) -> String { let mut cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index 04aed0a2..89d9a420 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -36,7 +36,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 191bf858..4a369423 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -36,7 +36,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index 0fa12aaf..456f6f38 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -48,7 +48,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index 96cbe55a..5b9f2161 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -38,7 +38,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index c7075aa6..8e236344 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -37,7 +37,7 @@ use utils::config::Config; const CONFIG: &str = r#" [directory."sql"] -protocol = "sql" +type = "sql" address = "sqlite://%PATH%/test.db?mode=rwc" [directory."sql".pool] @@ -46,7 +46,7 @@ min-connections = 0 idle-timeout = "5m" [directory."local"] -protocol = "memory" +type = "memory" [directory."local".lookup] invalid-ehlos = ["spammer.org", "spammer.net"] diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index bdeb3a2e..11f8ac71 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -92,7 +92,7 @@ set-body-length = false const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index 361f3c1c..1af49279 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -35,7 +35,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "john" diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index e417b5b4..fad04b25 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -41,7 +41,7 @@ use smtp::{ const CONFIG: &str = r#" [directory."sql"] -protocol = "sql" +type = "sql" address = "sqlite::memory:" [directory."sql".pool] diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index 15884020..223ced46 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -48,7 +48,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "admin" diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index 4d252e4f..c77dca1e 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -51,7 +51,7 @@ use smtp::{ const DIRECTORY: &str = r#" [directory."local"] -protocol = "memory" +type = "memory" [[directory."local".users]] name = "admin"