From 34e21ef03f898bcd440cedf2f53920e8ac2ba624 Mon Sep 17 00:00:00 2001 From: Me Date: Tue, 27 Jun 2023 18:28:07 +0200 Subject: [PATCH] IMAP commands append, close, copy, move, enable, expunge, login, logout, namespace and select implemented. --- .gitignore | 1 + Cargo.lock | 383 +++++++++-------- Cargo.toml | 3 + crates/imap-proto/src/protocol/select.rs | 2 +- crates/imap/src/core/mailbox.rs | 27 +- crates/imap/src/core/message.rs | 3 +- crates/imap/src/op/append.rs | 219 ++++++++++ crates/imap/src/op/close.rs | 45 ++ crates/imap/src/op/copy_move.rs | 519 +++++++++++++++++++++++ crates/imap/src/op/create.rs | 53 +-- crates/imap/src/op/delete.rs | 16 +- crates/imap/src/op/enable.rs | 80 ++++ crates/imap/src/op/expunge.rs | 263 ++++++++++++ crates/imap/src/op/fetch.rs | 57 ++- crates/imap/src/op/login.rs | 47 ++ crates/imap/src/op/logout.rs | 49 +++ crates/imap/src/op/mod.rs | 9 + crates/imap/src/op/namespace.rs | 52 +++ crates/imap/src/op/rename.rs | 10 +- crates/imap/src/op/select.rs | 167 ++++++++ crates/imap/src/op/store.rs | 57 +-- crates/imap/src/op/subscribe.rs | 2 +- crates/jmap-proto/src/error/set.rs | 2 +- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/changes/write.rs | 6 +- crates/jmap/src/email/copy.rs | 342 ++++++++------- crates/jmap/src/email/set.rs | 2 +- crates/jmap/src/identity/set.rs | 2 +- crates/jmap/src/mailbox/set.rs | 2 +- crates/jmap/src/sieve/set.rs | 2 +- crates/jmap/src/submission/set.rs | 2 +- crates/jmap/src/vacation/set.rs | 2 +- crates/main/src/main.rs | 7 + crates/smtp/Cargo.toml | 2 +- crates/utils/Cargo.toml | 2 +- tests/Cargo.toml | 5 +- tests/src/lib.rs | 7 + 37 files changed, 1972 insertions(+), 479 deletions(-) create mode 100644 crates/imap/src/op/append.rs create mode 100644 crates/imap/src/op/close.rs create mode 100644 crates/imap/src/op/copy_move.rs create mode 100644 crates/imap/src/op/enable.rs create mode 100644 crates/imap/src/op/expunge.rs create mode 100644 crates/imap/src/op/login.rs create mode 100644 crates/imap/src/op/logout.rs create mode 100644 crates/imap/src/op/namespace.rs create mode 100644 crates/imap/src/op/select.rs diff --git a/.gitignore b/.gitignore index b6a70171..f16e3f6c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ stalwart.toml run.sh _ignore +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 9f31c836..2a694df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,9 +20,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher 0.4.4", @@ -91,6 +91,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -131,9 +137,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "asn1-rs" @@ -148,7 +154,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.21", + "time 0.3.22", ] [[package]] @@ -182,7 +188,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -204,7 +210,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -215,7 +221,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -262,7 +268,7 @@ dependencies = [ "rust-ini", "serde", "thiserror", - "time 0.3.21", + "time 0.3.22", "url", ] @@ -405,13 +411,13 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "prettyplease 0.2.6", + "prettyplease 0.2.9", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.18", + "syn 2.0.22", "which", ] @@ -423,9 +429,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" [[package]] name = "bitpacking" @@ -447,15 +453,15 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", - "constant_time_eq 0.2.5", + "constant_time_eq 0.2.6", "digest 0.10.7", ] @@ -626,9 +632,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "constant_time_eq" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" [[package]] name = "core-foundation" @@ -648,9 +654,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -702,14 +708,14 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset 0.8.0", + "memoffset 0.9.0", "scopeguard", ] @@ -725,9 +731,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -895,14 +901,14 @@ dependencies = [ "password-hash 0.5.0", "pbkdf2 0.12.1", "pwhash", - "rustls 0.21.1", + "rustls 0.21.2", "scrypt", "sha1", - "sha2 0.10.6", + "sha2 0.10.7", "smtp-proto", "sqlx", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tracing", "utils", ] @@ -935,7 +941,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -978,7 +984,7 @@ dependencies = [ "once_cell", "openssl", "serde", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", ] @@ -1227,7 +1233,7 @@ checksum = "83c8d52fe8b46ab822b4decdcc0d6d85aeedfc98f0d52ba2bd4aec4a97807516" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", "try_map", ] @@ -1307,7 +1313,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -1410,9 +1416,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -1438,20 +1444,21 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash 0.8.3", + "allocator-api2", ] [[package]] name = "hashlink" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.0", ] [[package]] @@ -1643,9 +1650,9 @@ checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" dependencies = [ "http", "hyper 0.14.26", - "rustls 0.21.1", + "rustls 0.21.2", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", ] [[package]] @@ -1717,11 +1724,11 @@ dependencies = [ "mail-send", "md5", "parking_lot", - "rustls 0.21.1", + "rustls 0.21.2", "rustls-pemfile", "store", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tracing", "utils", ] @@ -1808,6 +1815,26 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jemalloc-sys" +version = "0.5.3+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd5d616ea7ed58b571b2e209a65759664d7fb021a0819d7a790afc67e47ca1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6" +dependencies = [ + "jemalloc-sys", + "libc", +] + [[package]] name = "jieba-rs" version = "0.6.7" @@ -1850,7 +1877,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "sieve-rs", "smtp", "smtp-proto", @@ -1866,7 +1893,7 @@ dependencies = [ [[package]] name = "jmap-client" version = "0.3.0" -source = "git+https://github.com/stalwartlabs/jmap-client#5434b2d26e7285a89146b708a710962b096b05bf" +source = "git+https://github.com/stalwartlabs/jmap-client#ab6a9e55c2008e18422b9346dbc5220ee0a47df3" dependencies = [ "ahash 0.8.3", "async-stream", @@ -1876,7 +1903,7 @@ dependencies = [ "maybe-async 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot", "reqwest", - "rustls 0.21.1", + "rustls 0.21.2", "serde", "serde_json", "tokio", @@ -1909,9 +1936,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1933,9 +1960,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lber" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5d85f5e00e12cb50c70c3b1c1f0daff6546eb4c608b44d0a990e38a539e0446" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" dependencies = [ "bytes", "nom", @@ -1943,9 +1970,9 @@ dependencies = [ [[package]] name = "ldap3" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5cfbd3c59ca16d6671b002b8b3dd013cd825d9c77a1664a3135194d3270511e" +checksum = "6dceb52eac140c8679326f619b963577ccf5a4ab025fe5e6db424d2fd3e4f400" dependencies = [ "async-trait", "bytes", @@ -1957,22 +1984,22 @@ dependencies = [ "nom", "percent-encoding", "ring", - "rustls 0.20.8", + "rustls 0.21.2", "rustls-native-certs", "thiserror", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", "tokio-stream", "tokio-util", "url", - "x509-parser 0.14.0", + "x509-parser", ] [[package]] name = "libc" -version = "0.2.146" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" @@ -2052,9 +2079,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "lru-cache" @@ -2123,10 +2150,10 @@ dependencies = [ "mail-builder", "md5", "rand", - "rustls 0.21.1", + "rustls 0.21.2", "smtp-proto", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "webpki-roots 0.23.1", ] @@ -2135,6 +2162,7 @@ name = "mail-server" version = "0.3.0" dependencies = [ "directory", + "jemallocator", "jmap", "jmap_proto", "smtp", @@ -2240,9 +2268,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -2411,9 +2439,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.54" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -2432,7 +2460,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2443,9 +2471,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.88" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", @@ -2582,7 +2610,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -2645,7 +2673,7 @@ dependencies = [ "digest 0.10.7", "hmac 0.12.1", "password-hash 0.4.2", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -2657,7 +2685,7 @@ dependencies = [ "digest 0.10.7", "hmac 0.12.1", "password-hash 0.5.0", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -2702,9 +2730,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", @@ -2712,9 +2740,9 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", @@ -2722,9 +2750,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", @@ -2732,22 +2760,22 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.22", ] [[package]] name = "phf_shared" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] @@ -2769,7 +2797,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2824,9 +2852,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "polyval" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" dependencies = [ "cfg-if", "cpufeatures", @@ -2852,12 +2880,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" dependencies = [ "proc-macro2", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -2871,9 +2899,9 @@ dependencies = [ [[package]] name = "privdrop" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ed9e5437d82d5f2cde999a21571474c5f09b3d76e33eab94bf0e8e42a4fd96" +checksum = "9bc12de3935536ed9b69488faea4450a298dac44179b54f71806e63f55034bf9" dependencies = [ "libc", "nix", @@ -2881,9 +2909,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] @@ -3138,14 +3166,13 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.1", - "rustls-native-certs", + "rustls 0.21.2", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -3245,7 +3272,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.3.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3287,9 +3314,9 @@ dependencies = [ "reqwest", "serde", "serde_derive", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", - "time 0.3.21", + "time 0.3.22", "tokio", "tokio-stream", "url", @@ -3322,9 +3349,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags 1.3.2", "errno", @@ -3348,9 +3375,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" dependencies = [ "log", "ring", @@ -3360,9 +3387,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -3443,7 +3470,7 @@ dependencies = [ "password-hash 0.5.0", "pbkdf2 0.12.1", "salsa20", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -3495,9 +3522,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] @@ -3513,20 +3540,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -3567,7 +3594,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -3609,9 +3636,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -3710,21 +3737,21 @@ dependencies = [ "rayon", "regex", "reqwest", - "rustls 0.21.1", + "rustls 0.21.2", "rustls-pemfile", "serde", "serde_json", "sha1", - "sha2 0.10.6", + "sha2 0.10.7", "sieve-rs", "smtp-proto", "sqlx", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tracing", "utils", "webpki-roots 0.23.1", - "x509-parser 0.15.0", + "x509-parser", ] [[package]] @@ -3840,11 +3867,11 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls 0.21.1", + "rustls 0.21.2", "rustls-pemfile", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "smallvec", "sqlformat", "thiserror", @@ -3883,7 +3910,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -3927,7 +3954,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2 0.10.6", + "sha2 0.10.7", "smallvec", "sqlx-core", "stringprep", @@ -3966,7 +3993,7 @@ dependencies = [ "serde", "serde_json", "sha1", - "sha2 0.10.6", + "sha2 0.10.7", "smallvec", "sqlx-core", "stringprep", @@ -4066,9 +4093,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" dependencies = [ "proc-macro2", "quote", @@ -4126,6 +4153,7 @@ dependencies = [ "hyper 1.0.0-rc.3", "imap", "imap_proto", + "jemallocator", "jmap", "jmap-client", "jmap_proto", @@ -4135,7 +4163,7 @@ dependencies = [ "num_cpus", "rayon", "reqwest", - "rustls 0.21.1", + "rustls 0.21.2", "rustls-pemfile", "serde", "serde_json", @@ -4146,7 +4174,7 @@ dependencies = [ "sqlx", "store", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tracing", "tracing-subscriber", "utils", @@ -4169,7 +4197,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -4195,9 +4223,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -4282,7 +4310,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -4298,11 +4326,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.1", + "rustls 0.21.2", "tokio", ] @@ -4325,9 +4353,9 @@ checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" dependencies = [ "futures-util", "log", - "rustls 0.21.1", + "rustls 0.21.2", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tungstenite", "webpki-roots 0.23.1", ] @@ -4443,19 +4471,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ "crossbeam-channel", - "time 0.3.21", + "time 0.3.22", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", ] [[package]] @@ -4599,7 +4627,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.21.1", + "rustls 0.21.2", "sha1", "thiserror", "url", @@ -4695,12 +4723,12 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "privdrop", - "rustls 0.21.1", + "rustls 0.21.2", "rustls-pemfile", "serde", "smtp-proto", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tracing", "tracing-appender", "tracing-opentelemetry", @@ -4709,9 +4737,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" [[package]] name = "valuable" @@ -4733,11 +4761,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -4755,9 +4782,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4765,24 +4792,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -4792,9 +4819,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4802,22 +4829,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.22", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-streams" @@ -4834,9 +4861,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -4893,9 +4920,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" [[package]] name = "widestring" @@ -5076,24 +5103,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "x509-parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" -dependencies = [ - "asn1-rs", - "base64 0.13.1", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror", - "time 0.3.21", -] - [[package]] name = "x509-parser" version = "0.15.0" @@ -5108,7 +5117,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.21", + "time 0.3.22", ] [[package]] @@ -5145,7 +5154,7 @@ dependencies = [ "hmac 0.12.1", "pbkdf2 0.11.0", "sha1", - "time 0.3.21", + "time 0.3.22", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index c67bf8c1..c0cc89f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,9 @@ utils = { path = "crates/utils" } tokio = { version = "1.23", features = ["full"] } tracing = "0.1" +[target.'cfg(not(target_env = "msvc"))'.dependencies] +jemallocator = "0.5.0" + [features] default = ["sqlite"] sqlite = ["store/sqlite"] diff --git a/crates/imap-proto/src/protocol/select.rs b/crates/imap-proto/src/protocol/select.rs index 643d7d40..fe88e713 100644 --- a/crates/imap-proto/src/protocol/select.rs +++ b/crates/imap-proto/src/protocol/select.rs @@ -51,7 +51,7 @@ pub struct Response { pub uid_next: u32, pub is_rev2: bool, pub closed_previous: bool, - pub highest_modseq: Option, + pub highest_modseq: Option, pub mailbox_id: String, } diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index b6db8c86..6b15a5a1 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -2,7 +2,11 @@ use std::{collections::BTreeMap, sync::atomic::Ordering}; use ahash::AHashMap; use imap_proto::{protocol::list::Attribute, StatusResponse}; -use jmap::{auth::AccessToken, mailbox::INBOX_ID, SUPERUSER_ID}; +use jmap::{ + auth::{acl::EffectiveAcl, AccessToken}, + mailbox::INBOX_ID, + SUPERUSER_ID, +}; use jmap_proto::{ object::Object, types::{acl::Acl, collection::Collection, property::Property, value::Value}, @@ -513,4 +517,25 @@ impl SessionData { pub fn is_all_mailbox(&self, mailbox_name: &str) -> bool { self.imap.name_all == mailbox_name } + + pub async fn check_mailbox_acl( + &self, + account_id: u32, + document_id: u32, + item: Acl, + ) -> crate::op::Result { + let access_token = self.get_access_token().await?; + Ok(access_token.is_member(account_id) + || self + .jmap + .get_property::>( + account_id, + Collection::Mailbox, + document_id, + Property::Value, + ) + .await? + .map(|mailbox| mailbox.effective_acl(&access_token).contains(item)) + .ok_or_else(|| StatusResponse::no("Mailbox no longer exists."))?) + } } diff --git a/crates/imap/src/core/message.rs b/crates/imap/src/core/message.rs index 01716995..c4a0b60c 100644 --- a/crates/imap/src/core/message.rs +++ b/crates/imap/src/core/message.rs @@ -279,6 +279,7 @@ impl SessionData { &self, mailbox: &SelectedMailbox, is_qresync: bool, + is_uid: bool, ) -> crate::op::Result> { // Obtain current modseq let modseq = self.get_modseq(mailbox.id.account_id).await?; @@ -297,7 +298,7 @@ impl SessionData { is_qresync, ids: deletions .into_iter() - .map(|id| if !is_qresync { id.seqnum } else { id.uid }) + .map(|id| if !is_uid { id.seqnum } else { id.uid }) .collect(), } .serialize_to(&mut buf); diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs new file mode 100644 index 00000000..a1b99a77 --- /dev/null +++ b/crates/imap/src/op/append.rs @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 std::sync::Arc; + +use imap_proto::{ + protocol::append::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, +}; + +use jmap::email::ingest::IngestEmail; +use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::TypeState}; +use mail_parser::Message; +use tokio::io::AsyncRead; + +use crate::core::{MailboxId, SelectedMailbox, Session, SessionData}; + +impl Session { + pub async fn handle_append(&mut self, request: Request) -> Result<(), ()> { + match request.parse_append() { + Ok(arguments) => { + let (data, selected_mailbox) = self.state.session_mailbox_state(); + + // Refresh mailboxes + if let Err(err) = data.synchronize_mailboxes(false).await { + return self + .write_bytes(err.with_tag(arguments.tag).into_bytes()) + .await; + } + + // Obtain mailbox + let mailbox = + if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) { + if mailbox.mailbox_id.is_some() { + mailbox + } else { + return self + .write_bytes( + StatusResponse::no( + "Appending messages to this mailbox is not allowed.", + ) + .with_tag(arguments.tag) + .with_code(ResponseCode::Cannot) + .into_bytes(), + ) + .await; + } + } else { + return self + .write_bytes( + StatusResponse::no("Mailbox does not exist.") + .with_tag(arguments.tag) + .with_code(ResponseCode::TryCreate) + .into_bytes(), + ) + .await; + }; + let is_qresync = self.is_qresync; + + tokio::spawn(async move { + data.write_bytes( + match data + .append_messages(arguments, selected_mailbox, mailbox, is_qresync) + .await + { + Ok(response) => response, + Err(response) => response, + } + .into_bytes(), + ) + .await; + }); + Ok(()) + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } +} + +impl SessionData { + async fn append_messages( + &self, + arguments: Arguments, + selected_mailbox: Option>, + mailbox: MailboxId, + is_qresync: bool, + ) -> crate::op::Result { + // Verify ACLs + let account_id = mailbox.account_id; + let mailbox_id = mailbox.mailbox_id.unwrap(); + if !self + .check_mailbox_acl(account_id, mailbox_id, Acl::AddItems) + .await + .map_err(|r| r.with_tag(&arguments.tag))? + { + return Ok(StatusResponse::no( + "You do not have the required permissions to append messages to this mailbox.", + ) + .with_tag(arguments.tag) + .with_code(ResponseCode::NoPerm)); + } + + // Obtain quota + let account_quota = self + .get_access_token() + .await + .map_err(|r| r.with_tag(&arguments.tag))? + .quota as i64; + + // Append messages + let mut response = StatusResponse::completed(Command::Append); + let mut created_ids = Vec::with_capacity(arguments.messages.len()); + let mut last_change_id = None; + for message in arguments.messages { + match self + .jmap + .email_ingest(IngestEmail { + raw_message: &message.message, + message: Message::parse(&message.message), + account_id, + account_quota, + mailbox_ids: vec![mailbox_id], + keywords: message.flags.into_iter().map(Keyword::from).collect(), + received_at: message.received_at.map(|d| d as u64), + skip_duplicates: false, + }) + .await + { + Ok(email) => { + created_ids.push(email.id.document_id()); + last_change_id = Some(email.change_id); + } + Err(err) => { + match err { + jmap::IngestError::Temporary => { + response = StatusResponse::database_failure(); + } + jmap::IngestError::OverQuota => { + response = StatusResponse::no("Disk quota exceeded.") + .with_code(ResponseCode::OverQuota); + } + jmap::IngestError::Permanent { reason, .. } => { + response = StatusResponse::no(reason); + } + } + break; + } + } + } + + // Broadcast changes + if let Some(change_id) = last_change_id { + self.jmap + .broadcast_state_change( + StateChange::new(account_id) + .with_change(TypeState::Email, change_id) + .with_change(TypeState::Mailbox, change_id) + .with_change(TypeState::Thread, change_id), + ) + .await; + } + + if !created_ids.is_empty() { + let (uids, uid_validity) = match selected_mailbox { + Some(selected_mailbox) if selected_mailbox.id == mailbox => { + self.synchronize_messages(&selected_mailbox, is_qresync, true) + .await + .map_err(|r| r.with_tag(&arguments.tag))?; + let mailbox = selected_mailbox.state.lock(); + ( + created_ids + .into_iter() + .filter_map(|id| mailbox.id_to_imap.get(&id)) + .map(|id| id.uid) + .collect(), + mailbox.uid_validity, + ) + } + + _ => { + let mailbox = self + .fetch_messages(&mailbox) + .await + .map_err(|r| r.with_tag(&arguments.tag))?; + ( + created_ids + .into_iter() + .filter_map(|id| mailbox.id_to_imap.get(&id)) + .map(|id| id.uid) + .collect(), + mailbox.uid_validity, + ) + } + }; + response = response.with_code(ResponseCode::AppendUid { uid_validity, uids }); + } + + Ok(response.with_tag(arguments.tag)) + } +} diff --git a/crates/imap/src/op/close.rs b/crates/imap/src/op/close.rs new file mode 100644 index 00000000..67f486e6 --- /dev/null +++ b/crates/imap/src/op/close.rs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 imap_proto::{receiver::Request, Command, StatusResponse}; + +use tokio::io::AsyncRead; + +use crate::core::{Session, State}; + +impl Session { + pub async fn handle_close(&mut self, request: Request) -> crate::OpResult { + let (data, mailbox) = self.state.select_data(); + if mailbox.is_select { + data.expunge(mailbox, None).await.ok(); + } + + self.state = State::Authenticated { data }; + self.write_bytes( + StatusResponse::completed(Command::Close) + .with_tag(request.tag) + .into_bytes(), + ) + .await + } +} diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs new file mode 100644 index 00000000..afa34690 --- /dev/null +++ b/crates/imap/src/op/copy_move.rs @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 std::sync::Arc; + +use imap_proto::{ + protocol::copy_move::Arguments, receiver::Request, Command, ResponseCode, ResponseType, + StatusResponse, +}; + +use jmap::email::set::TagManager; +use jmap_proto::{ + error::{method::MethodError, set::SetErrorType}, + types::{ + acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange, + type_state::TypeState, + }, +}; +use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE}; +use tokio::io::AsyncRead; + +use crate::core::{MailboxId, SelectedMailbox, Session, SessionData}; + +impl Session { + pub async fn handle_copy_move( + &mut self, + request: Request, + is_move: bool, + is_uid: bool, + ) -> crate::OpResult { + match request.parse_copy_move() { + Ok(arguments) => { + let (data, src_mailbox) = self.state.mailbox_state(); + + let is_qresync = self.is_qresync; + tokio::spawn(async move { + // Refresh mailboxes + if let Err(err) = data.synchronize_mailboxes(false).await { + return data + .write_bytes(err.with_tag(arguments.tag).into_bytes()) + .await; + } + + // Make sure the mailbox exists. + let dest_mailbox = + if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) { + if mailbox.mailbox_id.is_some() { + mailbox + } else { + return data + .write_bytes( + StatusResponse::no( + "Appending messages to this mailbox is not allowed.", + ) + .with_tag(arguments.tag) + .with_code(ResponseCode::Cannot) + .into_bytes(), + ) + .await; + } + } else { + return data + .write_bytes( + StatusResponse::no("Destination mailbox does not exist.") + .with_tag(arguments.tag) + .with_code(ResponseCode::TryCreate) + .into_bytes(), + ) + .await; + }; + + // Check that the destination mailbox is not the same as the source mailbox. + if src_mailbox.id.account_id == dest_mailbox.account_id + && src_mailbox.id.mailbox_id == dest_mailbox.mailbox_id + { + return data + .write_bytes( + StatusResponse::no( + "Source and destination mailboxes are the same.", + ) + .with_tag(arguments.tag) + .with_code(ResponseCode::Cannot) + .into_bytes(), + ) + .await; + } + + // Messages cannot me moved out of all folders. + if is_move && src_mailbox.id.mailbox_id.is_none() { + return data + .write_bytes( + StatusResponse::no( + "Moving messages out of this mailbox is not allowed.", + ) + .with_tag(arguments.tag) + .with_code(ResponseCode::Cannot) + .into_bytes(), + ) + .await; + } + + if let Err(err) = data + .copy_move( + arguments, + src_mailbox, + dest_mailbox, + is_move, + is_uid, + is_qresync, + ) + .await + { + data.write_bytes(err.into_bytes()).await; + } + + true + }); + Ok(()) + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } +} + +impl SessionData { + pub async fn copy_move( + &self, + arguments: Arguments, + src_mailbox: Arc, + dest_mailbox: MailboxId, + is_move: bool, + is_uid: bool, + is_qresync: bool, + ) -> Result<(), StatusResponse> { + // Convert IMAP ids to JMAP ids. + let ids = match src_mailbox + .sequence_to_ids(&arguments.sequence_set, is_uid) + .await + { + Ok(ids) => { + if ids.is_empty() { + return Err( + StatusResponse::no("No messages were found.").with_tag(arguments.tag) + ); + } + ids + } + Err(response) => { + return Err(response.with_tag(arguments.tag)); + } + }; + + // Verify that the user can delete messages from the source mailbox. + if is_move + && !self + .check_mailbox_acl( + src_mailbox.id.account_id, + src_mailbox.id.mailbox_id.unwrap_or_default(), + Acl::RemoveItems, + ) + .await + .map_err(|_| StatusResponse::database_failure().with_tag(&arguments.tag))? + { + return Err(StatusResponse::no( + "You do not have the required permissions to remove messages from the source mailbox.", + ) + .with_tag(arguments.tag).with_code(ResponseCode::NoPerm)); + } + + // Verify that the user can append messages to the destination mailbox. + let dest_mailbox_id = dest_mailbox.mailbox_id.unwrap(); + if !self + .check_mailbox_acl(dest_mailbox.account_id, dest_mailbox_id, Acl::AddItems) + .await + .map_err(|_| StatusResponse::database_failure().with_tag(&arguments.tag))? + { + return Err(StatusResponse::no( + "You do not have the required permissions to add messages to the destination mailbox.", + ) + .with_tag(arguments.tag).with_code(ResponseCode::NoPerm)); + } + + let mut response = StatusResponse::completed(if is_move { + Command::Move(is_uid) + } else { + Command::Copy(is_uid) + }); + let mut changelog = ChangeLogBuilder::new(); + let mut did_move = false; + let mut copied_ids = Vec::with_capacity(ids.len()); + if src_mailbox.id.account_id == dest_mailbox.account_id { + // Mailboxes are in the same account + let account_id = src_mailbox.id.account_id; + for (id, imap_id) in ids { + // Obtain mailbox tags + let (mut mailboxes, thread_id) = if let Some(result) = self + .get_mailbox_tags(account_id, id) + .await + .map_err(|_| StatusResponse::database_failure().with_tag(&arguments.tag))? + { + result + } else { + continue; + }; + // Make sure the message still belongs to this mailbox + if src_mailbox + .id + .mailbox_id + .map_or(false, |id| !mailboxes.current().contains(&id)) + || mailboxes.current().contains(&dest_mailbox_id) + { + tracing::debug!( + account_id = account_id, + document_id = id, + "Message does not belong to this mailbox" + ); + continue; + } + + // Add destination folder + mailboxes.update(dest_mailbox_id, true); + if is_move { + mailboxes.update(src_mailbox.id.mailbox_id.unwrap(), false); + } + + // Write changes + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Email) + .update_document(id); + mailboxes.update_batch(&mut batch, Property::MailboxIds); + if changelog.change_id == u64::MAX { + changelog.change_id = + self.jmap.assign_change_id(account_id).await.map_err(|_| { + StatusResponse::database_failure().with_tag(&arguments.tag) + })? + } + batch.value(Property::Cid, changelog.change_id, F_VALUE); + match self.jmap.write_batch(batch).await { + Ok(_) => { + changelog.log_update(Collection::Email, Id::from_parts(thread_id, id)); + changelog.log_child_update(Collection::Mailbox, dest_mailbox_id); + if is_move { + changelog.log_child_update( + Collection::Mailbox, + src_mailbox.id.mailbox_id.unwrap(), + ); + } + copied_ids.push((imap_id, id)); + } + Err(MethodError::ServerUnavailable) => { + response.rtype = ResponseType::No; + response.message = "Some messages could not be copied.".into(); + } + Err(_) => { + return Err(StatusResponse::database_failure().with_tag(&arguments.tag)); + } + } + } + } else { + // Obtain quota for target account + let src_account_id = src_mailbox.id.account_id; + let mut dest_change_id = None; + let dest_account_id = dest_mailbox.account_id; + let dest_quota = self + .jmap + .get_cached_access_token(dest_account_id) + .await + .ok_or_else(|| { + StatusResponse::no("Failed to obtain access token") + .with_code(ResponseCode::ContactAdmin) + })? + .quota as i64; + for (id, imap_id) in ids { + match self + .jmap + .copy_message( + src_account_id, + id, + dest_account_id, + dest_quota, + vec![dest_mailbox_id], + Vec::new(), + None, + ) + .await + { + Ok(Ok(email)) => { + dest_change_id = email.change_id.into(); + copied_ids.push((imap_id, email.id.document_id())); + } + Ok(Err(err)) => { + if err.type_ != SetErrorType::NotFound { + response.rtype = ResponseType::No; + response.code = Some(err.type_.into()); + if let Some(message) = err.description { + response.message = message; + } + } + continue; + } + Err(_) => { + return Err(StatusResponse::database_failure().with_tag(arguments.tag)) + } + }; + + if is_move { + // Obtain mailbox tags + let (mut mailboxes, thread_id) = if let Some(result) = self + .get_mailbox_tags(src_account_id, id) + .await + .map_err(|_| StatusResponse::database_failure().with_tag(&arguments.tag))? + { + result + } else { + continue; + }; + + // Make sure the message is still in the mailbox + let src_mailbox_id = src_mailbox.id.mailbox_id.unwrap(); + if !mailboxes.current().contains(&src_mailbox_id) { + continue; + } else if mailboxes.current().len() == 1 { + // Delete message if it is no longer in any mailbox + if let Ok(changes) = self + .jmap + .email_delete(src_account_id, id) + .await + .map_err(|_| { + StatusResponse::database_failure().with_tag(&arguments.tag) + })? + { + did_move = true; + changelog.merge(changes); + } + } else { + // Remove mailbox tag from message + let mut batch = BatchBuilder::new(); + batch + .with_account_id(src_account_id) + .with_collection(Collection::Email) + .update_document(id); + mailboxes.update(src_mailbox_id, false); + mailboxes.update_batch(&mut batch, Property::MailboxIds); + if changelog.change_id == u64::MAX { + changelog.change_id = self + .jmap + .assign_change_id(src_account_id) + .await + .map_err(|_| { + StatusResponse::database_failure().with_tag(&arguments.tag) + })? + } + batch.value(Property::Cid, changelog.change_id, F_VALUE); + match self.jmap.write_batch(batch).await { + Ok(_) => { + changelog + .log_update(Collection::Email, Id::from_parts(thread_id, id)); + changelog.log_child_update(Collection::Mailbox, src_mailbox_id); + did_move = true; + } + Err(MethodError::ServerUnavailable) => { + response.rtype = ResponseType::No; + response.message = "Some messages could not be moved.".into(); + } + Err(_) => { + return Err( + StatusResponse::database_failure().with_tag(&arguments.tag) + ); + } + } + } + } + } + + // Broadcast changes on destination account + if let Some(change_id) = dest_change_id { + self.jmap + .broadcast_state_change( + StateChange::new(dest_account_id) + .with_change(TypeState::Email, change_id) + .with_change(TypeState::Thread, change_id) + .with_change(TypeState::Mailbox, change_id), + ) + .await; + } + } + + // Write changes on source account + if !changelog.is_empty() { + let change_id = self + .jmap + .commit_changes(src_mailbox.id.account_id, changelog) + .await + .map_err(|_| { + StatusResponse::database_failure().with_tag(response.tag.as_ref().unwrap()) + })?; + self.jmap + .broadcast_state_change( + StateChange::new(src_mailbox.id.account_id) + .with_change(TypeState::Email, change_id) + .with_change(TypeState::Mailbox, change_id), + ) + .await; + } + + // Resynchronize source mailbox on a successful move + if did_move { + self.synchronize_messages(&src_mailbox, is_qresync, is_uid) + .await + .map_err(|r| r.with_tag(&arguments.tag))?; + } + + // Map copied JMAP Ids to IMAP UIDs in the destination folder. + if copied_ids.is_empty() { + return Err(if response.rtype != ResponseType::Ok { + response + } else { + StatusResponse::no("No messages were copied.") + } + .with_tag(arguments.tag)); + } + + let dest_mailbox = self + .fetch_messages(&dest_mailbox) + .await + .map_err(|r| r.with_tag(&arguments.tag))?; + + // Prepare response + let uid_validity = dest_mailbox.uid_validity; + let mut src_uids = Vec::with_capacity(copied_ids.len()); + let mut dest_uids = Vec::with_capacity(copied_ids.len()); + for (src_id, dest_id) in copied_ids { + if let Some(dest_uid) = dest_mailbox.id_to_imap.get(&dest_id) { + src_uids.push(src_id.uid); + dest_uids.push(dest_uid.uid); + } else { + tracing::debug!("Could not map JMAP ID {} to IMAP UID", dest_id); + } + } + src_uids.sort_unstable(); + dest_uids.sort_unstable(); + + self.write_bytes(if is_move { + response.with_tag(arguments.tag).serialize( + StatusResponse::ok("Copied UIDs") + .with_code(ResponseCode::CopyUid { + uid_validity, + src_uids, + dest_uids, + }) + .into_bytes(), + ) + } else { + response + .with_tag(arguments.tag) + .with_code(ResponseCode::CopyUid { + uid_validity, + src_uids, + dest_uids, + }) + .into_bytes() + }) + .await; + + Ok(()) + } + + pub async fn get_mailbox_tags( + &self, + account_id: u32, + id: u32, + ) -> Result, u32)>, MethodError> { + // Obtain mailbox tags + if let (Some(mailboxes), Some(thread_id)) = ( + self.jmap + .get_property::>>( + account_id, + Collection::Email, + id, + Property::MailboxIds, + ) + .await?, + self.jmap + .get_property::(account_id, Collection::Email, id, Property::ThreadId) + .await?, + ) { + Ok(Some((TagManager::new(mailboxes), thread_id))) + } else { + tracing::debug!( + account_id = account_id, + document_id = id, + "Message not found" + ); + Ok(None) + } + } +} diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs index b05269c1..a2a7f724 100644 --- a/crates/imap/src/op/create.rs +++ b/crates/imap/src/op/create.rs @@ -21,22 +21,17 @@ * for more details. */ -use std::sync::Arc; - use imap_proto::{ protocol::{create::Arguments, list::Attribute}, receiver::Request, Command, ResponseCode, StatusResponse, }; -use jmap::{ - auth::{acl::EffectiveAcl, AccessToken}, - mailbox::set::SCHEMA, -}; +use jmap::mailbox::set::SCHEMA; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange, - type_state::TypeState, value::Value, + type_state::TypeState, }, }; use store::{query::Filter, write::BatchBuilder}; @@ -45,7 +40,7 @@ use tokio::io::AsyncRead; use crate::core::{Account, Mailbox, Session, SessionData}; impl Session { - pub async fn handle_create(&mut self, requests: Vec>) -> Result<(), ()> { + pub async fn handle_create(&mut self, requests: Vec>) -> crate::OpResult { let mut arguments = Vec::with_capacity(requests.len()); for request in requests { @@ -340,37 +335,21 @@ impl SessionData { }; // Validate ACLs - let access_token = self.get_access_token().await?; - if access_token.is_shared(account_id) { - if let Some(parent_mailbox_id) = parent_mailbox_id { - if let Ok(Some(fields)) = self - .jmap - .get_property::>( - account_id, - Collection::Mailbox, - parent_mailbox_id, - Property::Value, - ) - .await - { - if !fields - .effective_acl(&access_token) - .contains_any([Acl::CreateChild, Acl::Administer].into_iter()) - { - return Err(StatusResponse::no( - "You are not allowed to create sub mailboxes under this mailbox.", - ) - .with_code(ResponseCode::NoPerm)); - } - } else { - return Err(StatusResponse::no("Action failed, please try again.")); - } - } else { + if let Some(parent_mailbox_id) = parent_mailbox_id { + if !self + .check_mailbox_acl(account_id, parent_mailbox_id, Acl::CreateChild) + .await? + { return Err(StatusResponse::no( - "You are not allowed to create root folders under shared folders.", + "You are not allowed to create sub mailboxes under this mailbox.", ) - .with_code(ResponseCode::Cannot)); + .with_code(ResponseCode::NoPerm)); } + } else { + return Err(StatusResponse::no( + "You are not allowed to create root folders under shared folders.", + ) + .with_code(ResponseCode::Cannot)); } Ok(CreateParams { @@ -402,7 +381,6 @@ impl SessionData { None }, is_rename: false, - access_token, }) } } @@ -415,6 +393,5 @@ pub struct CreateParams<'x> { pub parent_mailbox_id: Option, pub parent_mailbox_name: Option, pub special_use: Option, - pub access_token: Arc, pub is_rename: bool, } diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs index a95908bb..20a8e888 100644 --- a/crates/imap/src/op/delete.rs +++ b/crates/imap/src/op/delete.rs @@ -31,7 +31,7 @@ use tokio::io::AsyncRead; use crate::core::{Session, SessionData}; impl Session { - pub async fn handle_delete(&mut self, requests: Vec>) -> Result<(), ()> { + pub async fn handle_delete(&mut self, requests: Vec>) -> crate::OpResult { let mut arguments = Vec::with_capacity(requests.len()); for request in requests { @@ -82,10 +82,10 @@ impl SessionData { Ok(access_token) => access_token, Err(response) => return response.with_tag(arguments.tag), }; - let mut changes = ChangeLogBuilder::new(); + let mut changelog = ChangeLogBuilder::new(); let did_remove_emails = match self .jmap - .mailbox_destroy(account_id, mailbox_id, &mut changes, &access_token, true) + .mailbox_destroy(account_id, mailbox_id, &mut changelog, &access_token, true) .await { Ok(Ok(did_remove_emails)) => did_remove_emails, @@ -98,10 +98,12 @@ impl SessionData { }; // Write changes - let change_id = changes.change_id; - if self.jmap.commit_changes(account_id, changes).await.is_err() { - return StatusResponse::database_failure().with_tag(arguments.tag); - } + let change_id = match self.jmap.commit_changes(account_id, changelog).await { + Ok(change_id) => change_id, + Err(_) => { + return StatusResponse::database_failure().with_tag(arguments.tag); + } + }; // Broadcast changes self.jmap diff --git a/crates/imap/src/op/enable.rs b/crates/imap/src/op/enable.rs new file mode 100644 index 00000000..975ed6d5 --- /dev/null +++ b/crates/imap/src/op/enable.rs @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 imap_proto::{ + protocol::{capability::Capability, ProtocolVersion}, + receiver::Request, + Command, StatusResponse, +}; + +use tokio::io::AsyncRead; + +use crate::core::Session; + +impl Session { + pub async fn handle_enable(&mut self, request: Request) -> crate::OpResult { + match request.parse_enable() { + Ok(arguments) => { + for capability in arguments.capabilities { + match capability { + Capability::IMAP4rev2 => { + self.version = ProtocolVersion::Rev2; + } + Capability::IMAP4rev1 => { + self.version = ProtocolVersion::Rev1; + } + Capability::CondStore => { + self.is_condstore = true; + } + Capability::QResync => { + self.is_qresync = true; + } + Capability::Utf8Accept => {} + _ => { + let mut buf = Vec::with_capacity(10); + capability.serialize(&mut buf); + self.write_bytes( + StatusResponse::ok(format!( + "{} cannot be enabled.", + String::from_utf8(buf).unwrap() + )) + .with_tag(arguments.tag) + .into_bytes(), + ) + .await?; + return Ok(()); + } + } + } + + self.write_bytes( + StatusResponse::ok("ENABLE successful.") + .with_tag(arguments.tag) + .into_bytes(), + ) + .await + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } +} diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs new file mode 100644 index 00000000..3cd8fe36 --- /dev/null +++ b/crates/imap/src/op/expunge.rs @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 std::sync::Arc; + +use ahash::AHashMap; +use imap_proto::{ + parser::parse_sequence_set, + receiver::{Request, Token}, + Command, ResponseCode, StatusResponse, +}; + +use jmap::email::set::TagManager; +use jmap_proto::{ + error::method::MethodError, + types::{ + acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property, + state::StateChange, type_state::TypeState, + }, +}; +use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE}; +use tokio::io::AsyncRead; + +use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData}; + +impl Session { + pub async fn handle_expunge( + &mut self, + request: Request, + is_uid: bool, + ) -> crate::OpResult { + let (data, mailbox) = self.state.select_data(); + + // Validate ACL + match data + .check_mailbox_acl( + mailbox.id.account_id, + mailbox.id.mailbox_id.unwrap_or_default(), + Acl::RemoveItems, + ) + .await + { + Ok(true) => (), + Ok(false) => { + return self + .write_bytes(StatusResponse::no( + "You do not have the required permissions to remove messages from this mailbox.", + ) + .with_tag(request.tag) + .with_code(ResponseCode::NoPerm).into_bytes()) + .await; + } + Err(response) => { + return self + .write_bytes(response.with_tag(request.tag).into_bytes()) + .await; + } + } + + // Parse sequence to operate on + let sequence = if let Some(Token::Argument(value)) = request.tokens.into_iter().next() { + match parse_sequence_set(&value) { + Ok(sequence) => match mailbox.sequence_to_ids(&sequence, is_uid).await { + Ok(sequence) => Some(sequence), + Err(response) => { + return self + .write_bytes(response.with_tag(request.tag).into_bytes()) + .await; + } + }, + Err(err) => { + return self + .write_bytes(StatusResponse::bad(err).with_tag(request.tag).into_bytes()) + .await; + } + } + } else { + None + }; + + if let Err(response) = data.expunge(mailbox.clone(), sequence).await { + return self + .write_bytes(response.with_tag(request.tag).into_bytes()) + .await; + } + + // Clear saved searches + *mailbox.saved_search.lock() = SavedSearch::None; + + // Synchronize messages + match data + .synchronize_messages(&mailbox, self.is_qresync, is_uid) + .await + { + Ok(_) => { + self.write_bytes( + StatusResponse::completed(Command::Expunge(is_uid)) + .with_tag(request.tag) + .into_bytes(), + ) + .await + } + Err(response) => { + self.write_bytes(response.with_tag(request.tag).into_bytes()) + .await + } + } + } +} + +impl SessionData { + pub async fn expunge( + &self, + mailbox: Arc, + sequence: Option>, + ) -> crate::op::Result<()> { + // Obtain message ids + let account_id = mailbox.id.account_id; + let deleted_ids = if let Some(mailbox_id) = mailbox.id.mailbox_id { + self.jmap + .get_tag( + account_id, + Collection::Email, + Property::MailboxIds, + mailbox_id, + ) + .await? + .unwrap_or_default() + & self + .jmap + .get_tag( + account_id, + Collection::Email, + Property::Keywords, + Keyword::Deleted, + ) + .await? + .unwrap_or_default() + } else { + self.jmap + .get_tag( + account_id, + Collection::Email, + Property::Keywords, + Keyword::Deleted, + ) + .await? + .unwrap_or_default() + }; + + // Delete ids + let mut changelog = ChangeLogBuilder::new(); + for id in deleted_ids { + if sequence + .as_ref() + .map_or(false, |ids| !ids.contains_key(&id)) + { + continue; + } + + if let Some(mailbox_id) = mailbox.id.mailbox_id { + // If the message is present in multiple mailboxes, untag it from this mailbox. + let (mut mailboxes, thread_id) = + if let Some(result) = self.get_mailbox_tags(account_id, id).await? { + result + } else { + continue; + }; + if !mailboxes.current().contains(&mailbox_id) { + continue; + } else if mailboxes.current().len() > 1 { + // Remove deleted flag + let mut keywords = if let Some(keywords) = self + .jmap + .get_property::>>( + account_id, + Collection::Email, + id, + Property::Keywords, + ) + .await? + { + TagManager::new(keywords) + } else { + continue; + }; + + // Untag message from this mailbox and remove Deleted flag + mailboxes.update(mailbox_id, false); + keywords.update(Keyword::Deleted, false); + + // Write changes + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Email) + .update_document(id); + mailboxes.update_batch(&mut batch, Property::MailboxIds); + keywords.update_batch(&mut batch, Property::Keywords); + if changelog.change_id == u64::MAX { + changelog.change_id = self.jmap.assign_change_id(account_id).await? + } + batch.value(Property::Cid, changelog.change_id, F_VALUE); + match self.jmap.write_batch(batch).await { + Ok(_) => { + changelog.log_update(Collection::Email, Id::from_parts(thread_id, id)); + changelog.log_child_update(Collection::Mailbox, mailbox_id); + } + Err(MethodError::ServerUnavailable) => {} + Err(_) => { + return Err(StatusResponse::database_failure()); + } + } + } else { + // Delete message from all mailboxes + if let Ok(changes) = self.jmap.email_delete(account_id, id).await? { + changelog.merge(changes); + } + } + } else { + // Delete message from all mailboxes + if let Ok(changes) = self.jmap.email_delete(account_id, id).await? { + changelog.merge(changes); + } + } + } + + // Write changes on source account + if !changelog.is_empty() { + let change_id = self.jmap.commit_changes(account_id, changelog).await?; + self.jmap + .broadcast_state_change( + StateChange::new(account_id) + .with_change(TypeState::Email, change_id) + .with_change(TypeState::Mailbox, change_id) + .with_change(TypeState::Thread, change_id), + ) + .await; + } + + Ok(()) + } +} diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index f97bc366..87d19ce5 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -61,7 +61,7 @@ impl Session { &mut self, request: Request, is_uid: bool, - ) -> Result<(), ()> { + ) -> crate::OpResult { match request.parse_fetch() { Ok(arguments) => { let (data, mailbox) = self.state.select_data(); @@ -115,7 +115,7 @@ impl SessionData { let account_id = mailbox.id.account_id; let mut modseq = match { if is_uid { - self.synchronize_messages(&mailbox, is_qresync).await + self.synchronize_messages(&mailbox, is_qresync, true).await } else { // Don't synchronize if we're not using UIDs as seqnums might change. self.get_modseq(mailbox.id.account_id).await @@ -208,24 +208,6 @@ impl SessionData { arguments.attributes.push_unique(Attribute::ModSeq); } - // Obtain shared messages - let access_token = match self.get_access_token().await { - Ok(access_token) => access_token, - Err(response) => return response.with_tag(arguments.tag), - }; - let can_modify_ids = if access_token.is_shared(account_id) { - match self - .jmap - .shared_messages(&access_token, account_id, Acl::ModifyItems) - .await - { - Ok(document_ids) => document_ids.into(), - Err(_) => return StatusResponse::database_failure().with_tag(arguments.tag), - } - } else { - None - }; - // Build properties list let mut set_seen_flags = false; let mut needs_thread_id = false; @@ -265,6 +247,20 @@ impl SessionData { _ => (), } } + + if set_seen_flags + && !self + .check_mailbox_acl( + mailbox.id.account_id, + mailbox.id.mailbox_id.unwrap_or_default(), + Acl::ModifyItems, + ) + .await + .unwrap_or(false) + { + set_seen_flags = false; + } + if is_uid { arguments.attributes.push_unique(Attribute::Uid); } @@ -348,9 +344,8 @@ impl SessionData { // Build response let mut items = Vec::with_capacity(arguments.attributes.len()); - let set_seen_flag = set_seen_flags - && !keywords.inner.iter().any(|k| k == &Keyword::Seen) - && can_modify_ids.as_ref().map_or(true, |ids| ids.contains(id)); + let set_seen_flag = + set_seen_flags && !keywords.inner.iter().any(|k| k == &Keyword::Seen); let thread_id = if needs_thread_id || set_seen_flag { if let Ok(Some(thread_id)) = self .jmap @@ -568,15 +563,13 @@ impl SessionData { } } if !changelog.is_empty() { - let change_id = changelog.change_id; - if self - .jmap - .commit_changes(account_id, changelog) - .await - .is_err() - { - return StatusResponse::database_failure().with_tag(arguments.tag); - } + // Write changes + let change_id = match self.jmap.commit_changes(account_id, changelog).await { + Ok(change_id) => change_id, + Err(_) => { + return StatusResponse::database_failure().with_tag(arguments.tag); + } + }; modseq = change_id.into(); self.jmap .broadcast_state_change( diff --git a/crates/imap/src/op/login.rs b/crates/imap/src/op/login.rs new file mode 100644 index 00000000..9a62a7bb --- /dev/null +++ b/crates/imap/src/op/login.rs @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 imap_proto::{receiver::Request, Command}; + +use mail_send::Credentials; +use tokio::io::AsyncRead; + +use crate::core::Session; + +impl Session { + pub async fn handle_login(&mut self, request: Request) -> Result<(), ()> { + match request.parse_login() { + Ok(args) => { + self.authenticate( + Credentials::Plain { + username: args.username, + secret: args.password, + }, + args.tag, + ) + .await + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } +} diff --git a/crates/imap/src/op/logout.rs b/crates/imap/src/op/logout.rs new file mode 100644 index 00000000..3130fb57 --- /dev/null +++ b/crates/imap/src/op/logout.rs @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 imap_proto::{receiver::Request, Command, StatusResponse}; + +use tokio::io::AsyncRead; + +use crate::core::Session; + +impl Session { + pub async fn handle_logout(&mut self, request: Request) -> crate::OpResult { + let mut response = StatusResponse::bye( + concat!( + "Stalwart IMAP4rev2 v", + env!("CARGO_PKG_VERSION"), + " bids you farewell." + ) + .to_string(), + ) + .into_bytes(); + response.extend( + StatusResponse::completed(Command::Logout) + .with_tag(request.tag) + .into_bytes(), + ); + self.write_bytes(response).await?; + Err(()) + } +} diff --git a/crates/imap/src/op/mod.rs b/crates/imap/src/op/mod.rs index 2925c5d0..74ba4ed9 100644 --- a/crates/imap/src/op/mod.rs +++ b/crates/imap/src/op/mod.rs @@ -1,12 +1,21 @@ use ::store::query::log::Query; use imap_proto::StatusResponse; +pub mod append; pub mod authenticate; +pub mod close; +pub mod copy_move; pub mod create; pub mod delete; +pub mod enable; +pub mod expunge; pub mod fetch; pub mod list; +pub mod login; +pub mod logout; +pub mod namespace; pub mod rename; +pub mod select; pub mod status; pub mod store; pub mod subscribe; diff --git a/crates/imap/src/op/namespace.rs b/crates/imap/src/op/namespace.rs new file mode 100644 index 00000000..315f80c3 --- /dev/null +++ b/crates/imap/src/op/namespace.rs @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 imap_proto::{ + protocol::{namespace::Response, ImapResponse}, + receiver::Request, + Command, StatusResponse, +}; + +use tokio::io::AsyncRead; + +use crate::core::Session; + +impl Session { + pub async fn handle_namespace(&mut self, request: Request) -> crate::OpResult { + self.write_bytes( + StatusResponse::completed(Command::Namespace) + .with_tag(request.tag) + .serialize( + Response { + shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 { + self.imap.name_shared.clone().into() + } else { + None + }, + } + .serialize(), + ), + ) + .await + } +} diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs index 14fcd660..40312639 100644 --- a/crates/imap/src/op/rename.rs +++ b/crates/imap/src/op/rename.rs @@ -41,7 +41,7 @@ use tokio::io::AsyncRead; use crate::core::{Session, SessionData}; impl Session { - pub async fn handle_rename(&mut self, request: Request) -> Result<(), ()> { + pub async fn handle_rename(&mut self, request: Request) -> crate::OpResult { match request.parse_rename(self.version) { Ok(arguments) => { let data = self.state.session_data(); @@ -121,10 +121,14 @@ impl SessionData { }; // Validate ACL - if params.access_token.is_shared(params.account_id) + let access_token = match self.get_access_token().await { + Ok(access_token) => access_token, + Err(response) => return response.with_tag(arguments.tag), + }; + if access_token.is_shared(params.account_id) && !mailbox .inner - .effective_acl(¶ms.access_token) + .effective_acl(&access_token) .contains(Acl::Modify) { return StatusResponse::no("You are not allowed to rename this mailbox.") diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs new file mode 100644 index 00000000..e8ed1915 --- /dev/null +++ b/crates/imap/src/op/select.rs @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP 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 std::sync::Arc; + +use imap_proto::{ + protocol::{fetch, list::ListItem, select::Response, ImapResponse}, + receiver::Request, + Command, ResponseCode, StatusResponse, +}; + +use jmap_proto::types::id::Id; +use tokio::io::AsyncRead; + +use crate::core::{SavedSearch, SelectedMailbox, Session, State}; + +impl Session { + pub async fn handle_select(&mut self, request: Request) -> crate::OpResult { + let is_select = request.command == Command::Select; + let command = request.command; + match request.parse_select(self.version) { + Ok(arguments) => { + let data = self.state.session_data(); + + // Refresh mailboxes + if let Err(err) = data.synchronize_mailboxes(false).await { + return self + .write_bytes(err.with_tag(arguments.tag).into_bytes()) + .await; + } + + if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) { + // Syncronize messages + match data.fetch_messages(&mailbox).await { + Ok(state) => { + let closed_previous = self.state.is_mailbox_selected(); + let is_condstore = self.is_condstore || arguments.condstore; + + // Build new state + let uid_validity = state.uid_validity; + let uid_next = state.uid_next; + let total_messages = state.total_messages; + let highest_modseq = state.last_state; + let mailbox = Arc::new(SelectedMailbox { + id: mailbox, + state: parking_lot::Mutex::new(state), + saved_search: parking_lot::Mutex::new(SavedSearch::None), + is_select, + is_condstore, + }); + + // Validate QRESYNC arguments + if let Some(qresync) = arguments.qresync { + if !self.is_qresync { + return self + .write_bytes( + StatusResponse::no("QRESYNC is not enabled.") + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + } + if qresync.uid_validity == uid_validity { + // Send flags for changed messages + data.fetch( + fetch::Arguments { + tag: String::new(), + sequence_set: qresync + .known_uids + .unwrap_or_else(|| qresync.seq_match.unwrap().1), + attributes: vec![fetch::Attribute::Flags], + changed_since: qresync.modseq.into(), + include_vanished: true, + }, + mailbox.clone(), + true, + true, + false, + ) + .await; + } + } + + // Build response + let response = Response { + mailbox: ListItem::new(arguments.mailbox_name), + total_messages, + recent_messages: 0, + unseen_seq: 0, + uid_validity, + uid_next, + closed_previous, + is_rev2: self.version.is_rev2(), + highest_modseq: highest_modseq.map(|id| id + 1), + mailbox_id: Id::from_parts( + mailbox.id.account_id, + mailbox.id.mailbox_id.unwrap_or(u32::MAX), + ) + .to_string(), + }; + + // Update state + self.state = State::Selected { data, mailbox }; + + self.write_bytes( + StatusResponse::completed(command) + .with_tag(arguments.tag) + .with_code(if is_select { + ResponseCode::ReadWrite + } else { + ResponseCode::ReadOnly + }) + .serialize(response.serialize()), + ) + .await + } + Err(mut response) => { + response.tag = arguments.tag.into(); + self.write_bytes(response.into_bytes()).await + } + } + } else { + self.write_bytes( + StatusResponse::no("Mailbox does not exist.") + .with_tag(arguments.tag) + .with_code(ResponseCode::NonExistent) + .into_bytes(), + ) + .await + } + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } + + pub async fn handle_unselect(&mut self, request: Request) -> Result<(), ()> { + self.state = State::Authenticated { + data: self.state.session_data(), + }; + self.write_bytes( + StatusResponse::completed(Command::Unselect) + .with_tag(request.tag) + .into_bytes(), + ) + .await + } +} diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index 19644b56..61aa4954 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -56,7 +56,7 @@ impl Session { &mut self, request: Request, is_uid: bool, - ) -> Result<(), ()> { + ) -> crate::OpResult { match request.parse_store() { Ok(arguments) => { let (data, mailbox) = self.state.select_data(); @@ -93,7 +93,7 @@ impl SessionData { let account_id = mailbox.id.account_id; if is_uid { // Don't synchronize if we're not using UIDs as seqnums might change. - self.synchronize_messages(&mailbox, is_qresync) + self.synchronize_messages(&mailbox, is_qresync, true) .await .map_err(|r| r.with_tag(&arguments.tag))?; } @@ -116,23 +116,22 @@ impl SessionData { } }; - // Obtain shared messages - let access_token = match self.get_access_token().await { - Ok(access_token) => access_token, - Err(response) => return Err(response.with_tag(arguments.tag)), - }; - let can_modify_ids = if access_token.is_shared(account_id) { - match self - .jmap - .shared_messages(&access_token, account_id, Acl::ModifyItems) - .await - { - Ok(document_ids) => document_ids.into(), - Err(_) => return Err(StatusResponse::database_failure().with_tag(arguments.tag)), - } - } else { - None - }; + // Verify that the user can modify messages in this mailbox. + if !self + .check_mailbox_acl( + mailbox.id.account_id, + mailbox.id.mailbox_id.unwrap_or_default(), + Acl::ModifyItems, + ) + .await + .map_err(|_| StatusResponse::database_failure().with_tag(&arguments.tag))? + { + return Err(StatusResponse::no( + "You do not have the required permissions to modify messages in this mailbox.", + ) + .with_tag(arguments.tag) + .with_code(ResponseCode::NoPerm)); + } // Filter out unchanged since ids let mut response_code = None; @@ -207,16 +206,6 @@ impl SessionData { let mut changelog = ChangeLogBuilder::new(); let mut changed_mailboxes = AHashSet::new(); for (id, imap_id) in ids { - // Check ACLs - if can_modify_ids - .as_ref() - .map_or(false, |can_modify_ids| !can_modify_ids.contains(id)) - { - response.rtype = ResponseType::No; - response.message = "Not enough permissions to modify one or more messages.".into(); - continue; - } - // Obtain current keywords let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = ( self.jmap @@ -367,17 +356,13 @@ impl SessionData { // Write changes if !changelog.is_empty() { - let change_id = changelog.change_id; - if self + let change_id = self .jmap .commit_changes(account_id, changelog) .await - .is_err() - { - return Err( + .map_err(|_| { StatusResponse::database_failure().with_tag(response.tag.as_ref().unwrap()) - ); - } + })?; self.jmap .broadcast_state_change(if !changed_mailboxes.is_empty() { StateChange::new(account_id) diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs index ad9fa6f0..24e54705 100644 --- a/crates/imap/src/op/subscribe.rs +++ b/crates/imap/src/op/subscribe.rs @@ -41,7 +41,7 @@ impl Session { &mut self, request: Request, is_subscribe: bool, - ) -> Result<(), ()> { + ) -> crate::OpResult { match request.parse_subscribe(self.version) { Ok(arguments) => { let data = self.state.session_data(); diff --git a/crates/jmap-proto/src/error/set.rs b/crates/jmap-proto/src/error/set.rs index 13581991..7e28e418 100644 --- a/crates/jmap-proto/src/error/set.rs +++ b/crates/jmap-proto/src/error/set.rs @@ -47,7 +47,7 @@ pub enum InvalidProperty { Path(Vec), } -#[derive(Debug, Clone, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] pub enum SetErrorType { #[serde(rename = "forbidden")] Forbidden, diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index cd59a635..5b82c54a 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -34,7 +34,7 @@ base64 = "0.21" p256 = { version = "0.13", features = ["ecdh"] } hkdf = "0.12.3" sha2 = "0.10.1" -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"]} +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]} tokio-tungstenite = "0.19.0" tungstenite = "0.19.0" chrono = "0.4" diff --git a/crates/jmap/src/changes/write.rs b/crates/jmap/src/changes/write.rs index 8a5134e5..5ff7e9b2 100644 --- a/crates/jmap/src/changes/write.rs +++ b/crates/jmap/src/changes/write.rs @@ -21,7 +21,7 @@ * for more details. */ -use jmap_proto::{error::method::MethodError, types::state::State}; +use jmap_proto::error::method::MethodError; use store::write::{log::ChangeLogBuilder, BatchBuilder}; use crate::JMAP; @@ -51,11 +51,11 @@ impl JMAP { &self, account_id: u32, mut changes: ChangeLogBuilder, - ) -> Result { + ) -> Result { if changes.change_id == u64::MAX { changes.change_id = self.assign_change_id(account_id).await?; } - let state = State::from(changes.change_id); + let state = changes.change_id; let mut builder = BatchBuilder::new(); builder.with_account_id(account_id).custom(changes); diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 98abbcf5..7d2258f1 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -38,7 +38,9 @@ use jmap_proto::{ acl::Acl, blob::BlobId, collection::Collection, + date::UTCDate, id::Id, + keyword::Keyword, property::Property, state::{State, StateChange}, type_state::TypeState, @@ -218,170 +220,26 @@ impl JMAP { } } - // Obtain term index and metadata - let (mut metadata, token_index) = if let (Some(metadata), Some(token_index)) = ( - self.get_property::>( + // Add response + match self + .copy_message( from_account_id, - Collection::Email, from_message_id, - Property::BodyStructure, + account_id, + account_quota, + mailboxes, + keywords, + received_at, ) - .await?, - self.get_term_index::>( - from_account_id, - Collection::Email, - from_message_id, - ) - .await?, - ) { - (metadata, token_index) - } else { - response.not_created.append( - id, - SetError::not_found().with_description(format!( - "Item {} not found not found in account {}.", - id, response.from_account_id - )), - ); - continue; - }; - - // Check quota - if account_quota > 0 - && metadata.get(&Property::Size).as_uint().unwrap_or_default() as i64 - + self.get_used_quota(account_id).await? - > account_quota + .await? { - response.not_created.append(id, SetError::over_quota()); - continue; - } - - // Set receivedAt - if let Some(received_at) = received_at { - metadata.set(Property::ReceivedAt, Value::Date(received_at)); - } - - // Obtain threadId - let mut references = vec![]; - let mut subject = ""; - for (property, value) in &metadata.properties { - match property { - Property::MessageId - | Property::InReplyTo - | Property::References - | Property::EmailIds => match value { - Value::Text(text) => { - references.push(text.as_str()); - } - Value::List(list) => { - references.extend(list.iter().filter_map(|v| v.as_string())); - } - _ => (), - }, - Property::Subject => { - if let Some(value) = value.as_string() { - subject = thread_name(value).trim_text(MAX_SORT_FIELD_LENGTH); - } - if subject.is_empty() { - subject = "!"; - } - } - _ => (), + Ok(email) => { + response.created.append(id, email.into()); + } + Err(err) => { + response.not_created.append(id, err); } } - let thread_id = if !references.is_empty() { - self.find_or_merge_thread(account_id, subject, &references) - .await - .map_err(|_| MethodError::ServerPartialFail)? - } else { - None - }; - - // Copy blob - let message_id = self - .assign_document_id(account_id, Collection::Email) - .await?; - let mut email = IngestedEmail { - blob_id: BlobId::new(BlobKind::LinkedMaildir { - account_id, - document_id: message_id, - }), - size: metadata.get(&Property::Size).as_uint().unwrap_or(0) as usize, - ..Default::default() - }; - self.store - .copy_blob( - &BlobKind::LinkedMaildir { - account_id: from_account_id, - document_id: from_message_id, - }, - &email.blob_id.kind, - None, - ) - .await - .map_err(|err| { - tracing::error!( - event = "error", - context = "email_copy", - from_account_id = from_account_id, - from_message_id = from_message_id, - account_id = account_id, - message_id = message_id, - error = ?err, - "Failed to copy blob."); - MethodError::ServerPartialFail - })?; - - // Prepare batch - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id); - - // Build change log - let mut changes = self.begin_changes(account_id).await?; - let thread_id = if let Some(thread_id) = thread_id { - changes.log_child_update(Collection::Thread, thread_id); - thread_id - } else { - let thread_id = self - .assign_document_id(account_id, Collection::Thread) - .await?; - batch - .with_collection(Collection::Thread) - .create_document(thread_id); - changes.log_insert(Collection::Thread, thread_id); - thread_id - }; - email.id = Id::from_parts(thread_id, message_id); - email.change_id = changes.change_id; - changes.log_insert(Collection::Email, email.id); - for mailbox_id in &mailboxes { - changes.log_child_update(Collection::Mailbox, *mailbox_id); - } - - // Build batch - batch - .with_collection(Collection::Email) - .create_document(message_id) - .value(Property::ThreadId, thread_id, F_VALUE | F_BITMAP) - .value(Property::MailboxIds, mailboxes, F_VALUE | F_BITMAP) - .value(Property::Keywords, keywords, F_VALUE | F_BITMAP) - .custom(EmailIndexBuilder::set(metadata)) - .custom(token_index) - .custom(changes); - self.store.write(batch.build()).await.map_err(|err| { - tracing::error!( - event = "error", - context = "email_copy", - error = ?err, - "Failed to write message to database."); - MethodError::ServerPartialFail - })?; - - // Update state - response.new_state = email.change_id.into(); - - // Add response - response.created.append(id, email.into()); // Add to destroy list if on_success_delete { @@ -420,4 +278,172 @@ impl JMAP { Ok(response) } + + pub async fn copy_message( + &self, + from_account_id: u32, + from_message_id: u32, + account_id: u32, + account_quota: i64, + mailboxes: Vec, + keywords: Vec, + received_at: Option, + ) -> Result, MethodError> { + // Obtain term index and metadata + let (mut metadata, token_index) = if let (Some(metadata), Some(token_index)) = ( + self.get_property::>( + from_account_id, + Collection::Email, + from_message_id, + Property::BodyStructure, + ) + .await?, + self.get_term_index::>( + from_account_id, + Collection::Email, + from_message_id, + ) + .await?, + ) { + (metadata, token_index) + } else { + return Ok(Err(SetError::not_found().with_description(format!( + "Message not found not found in account {}.", + Id::from(from_account_id) + )))); + }; + + // Check quota + if account_quota > 0 + && metadata.get(&Property::Size).as_uint().unwrap_or_default() as i64 + + self.get_used_quota(account_id).await? + > account_quota + { + return Ok(Err(SetError::over_quota())); + } + + // Set receivedAt + if let Some(received_at) = received_at { + metadata.set(Property::ReceivedAt, Value::Date(received_at)); + } + + // Obtain threadId + let mut references = vec![]; + let mut subject = ""; + for (property, value) in &metadata.properties { + match property { + Property::MessageId + | Property::InReplyTo + | Property::References + | Property::EmailIds => match value { + Value::Text(text) => { + references.push(text.as_str()); + } + Value::List(list) => { + references.extend(list.iter().filter_map(|v| v.as_string())); + } + _ => (), + }, + Property::Subject => { + if let Some(value) = value.as_string() { + subject = thread_name(value).trim_text(MAX_SORT_FIELD_LENGTH); + } + if subject.is_empty() { + subject = "!"; + } + } + _ => (), + } + } + let thread_id = if !references.is_empty() { + self.find_or_merge_thread(account_id, subject, &references) + .await + .map_err(|_| MethodError::ServerPartialFail)? + } else { + None + }; + + // Copy blob + let message_id = self + .assign_document_id(account_id, Collection::Email) + .await?; + let mut email = IngestedEmail { + blob_id: BlobId::new(BlobKind::LinkedMaildir { + account_id, + document_id: message_id, + }), + size: metadata.get(&Property::Size).as_uint().unwrap_or(0) as usize, + ..Default::default() + }; + self.store + .copy_blob( + &BlobKind::LinkedMaildir { + account_id: from_account_id, + document_id: from_message_id, + }, + &email.blob_id.kind, + None, + ) + .await + .map_err(|err| { + tracing::error!( + event = "error", + context = "email_copy", + from_account_id = from_account_id, + from_message_id = from_message_id, + account_id = account_id, + message_id = message_id, + error = ?err, + "Failed to copy blob."); + MethodError::ServerPartialFail + })?; + + // Prepare batch + let mut batch = BatchBuilder::new(); + batch.with_account_id(account_id); + + // Build change log + let mut changes = self.begin_changes(account_id).await?; + let thread_id = if let Some(thread_id) = thread_id { + changes.log_child_update(Collection::Thread, thread_id); + thread_id + } else { + let thread_id = self + .assign_document_id(account_id, Collection::Thread) + .await?; + batch + .with_collection(Collection::Thread) + .create_document(thread_id); + changes.log_insert(Collection::Thread, thread_id); + thread_id + }; + email.id = Id::from_parts(thread_id, message_id); + email.change_id = changes.change_id; + changes.log_insert(Collection::Email, email.id); + for mailbox_id in &mailboxes { + changes.log_child_update(Collection::Mailbox, *mailbox_id); + } + + // Build batch + batch + .with_collection(Collection::Email) + .create_document(message_id) + .value(Property::ThreadId, thread_id, F_VALUE | F_BITMAP) + .value(Property::MailboxIds, mailboxes, F_VALUE | F_BITMAP) + .value(Property::Keywords, keywords, F_VALUE | F_BITMAP) + .value(Property::Cid, changes.change_id, F_VALUE) + .custom(EmailIndexBuilder::set(metadata)) + .custom(token_index); + + self.store.write(batch.build()).await.map_err(|err| { + tracing::error!( + event = "error", + context = "email_copy", + error = ?err, + "Failed to write message to database."); + MethodError::ServerPartialFail + })?; + + Ok(Ok(email)) + } } diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 5b7af1a5..082c4b9b 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -1024,7 +1024,7 @@ impl JMAP { // Update state if !changes.is_empty() || !response.created.is_empty() { let new_state = if !changes.is_empty() { - self.commit_changes(account_id, changes).await? + self.commit_changes(account_id, changes).await?.into() } else { self.get_state(account_id, Collection::Email).await? }; diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index cc9c0726..d966c140 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -191,7 +191,7 @@ impl JMAP { // Write changes if !changes.is_empty() { - response.new_state = self.commit_changes(account_id, changes).await?.into(); + response.new_state = Some(self.commit_changes(account_id, changes).await?.into()); } Ok(response) diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index f3aae3c4..422b537e 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -253,7 +253,7 @@ impl JMAP { state_change } .into(); - ctx.response.new_state = self.commit_changes(account_id, changes).await?.into(); + ctx.response.new_state = Some(self.commit_changes(account_id, changes).await?.into()); } Ok(ctx.response) diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index ab16d855..276d1651 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -249,7 +249,7 @@ impl JMAP { // Write changes if !changes.is_empty() { - ctx.response.new_state = self.commit_changes(account_id, changes).await?.into(); + ctx.response.new_state = Some(self.commit_changes(account_id, changes).await?.into()); } // Activate / deactivate scripts diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index 462349b1..e761e455 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -266,7 +266,7 @@ impl JMAP { // Write changes if !changes.is_empty() { - response.new_state = self.commit_changes(account_id, changes).await?.into(); + response.new_state = Some(self.commit_changes(account_id, changes).await?.into()); } // On success diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index 88aa270c..8b76075f 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -297,7 +297,7 @@ impl JMAP { // Write changes if !change_log.is_empty() { - response.new_state = self.commit_changes(account_id, change_log).await?.into(); + response.new_state = Some(self.commit_changes(account_id, change_log).await?.into()); } Ok(response) diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 0187c536..0171c96d 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -32,6 +32,13 @@ use utils::{ enable_tracing, wait_for_shutdown, UnwrapFailure, }; +#[cfg(not(target_env = "msvc"))] +use jemallocator::Jemalloc; + +#[cfg(not(target_env = "msvc"))] +#[global_allocator] +static GLOBAL: Jemalloc = Jemalloc; + #[tokio::main] async fn main() -> std::io::Result<()> { let config = Config::init(); diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 8c087678..9b1cf890 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -41,7 +41,7 @@ lru-cache = "0.1.2" rand = "0.8.5" x509-parser = "0.15.0" sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] } -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] } +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "blocking"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" num_cpus = "1.15.0" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index d24180c1..320d5e3f 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -18,7 +18,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" tracing-opentelemetry = "0.18.0" opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.11.0", features = ["http-proto", "reqwest-client", "reqwest-rustls"] } +opentelemetry-otlp = { version = "0.11.0", features = ["http-proto", "reqwest-client"] } opentelemetry-semantic-conventions = { version = "0.10.0" } [target.'cfg(unix)'.dependencies] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 1f9c086a..b389f44d 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -35,7 +35,7 @@ serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"]} +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]} bytes = "1.4.0" futures = "0.3" ece = "2.2" @@ -49,3 +49,6 @@ sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgr num_cpus = "1.15.0" async-trait = "0.1.68" chrono = "0.4" + +[target.'cfg(not(target_env = "msvc"))'.dependencies] +jemallocator = "0.5.0" diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 0d265fbe..e5b24e5d 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -23,6 +23,13 @@ use std::path::PathBuf; +#[cfg(not(target_env = "msvc"))] +use jemallocator::Jemalloc; + +#[cfg(not(target_env = "msvc"))] +#[global_allocator] +static GLOBAL: Jemalloc = Jemalloc; + #[cfg(test)] pub mod directory; #[cfg(test)]