From 4ff2158783ea4454143c2946c711df958ecb5f67 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Tue, 23 May 2023 13:49:21 +0000 Subject: [PATCH] EmailSubmission tests passing --- Cargo.lock | 463 +++++++--------- crates/jmap-proto/src/method/set.rs | 4 + crates/jmap-proto/src/object/index.rs | 16 +- crates/jmap-proto/src/types/property.rs | 107 +++- crates/jmap-proto/src/types/value.rs | 20 + crates/jmap/Cargo.toml | 1 + crates/jmap/src/api/config.rs | 6 +- crates/jmap/src/api/http.rs | 4 +- crates/jmap/src/api/request.rs | 37 +- crates/jmap/src/api/session.rs | 13 + crates/jmap/src/auth/acl.rs | 20 +- crates/jmap/src/auth/rate_limit.rs | 1 + crates/jmap/src/changes/query.rs | 5 +- crates/jmap/src/email/get.rs | 2 +- crates/jmap/src/email/import.rs | 6 +- crates/jmap/src/email/ingest.rs | 32 +- crates/jmap/src/identity/get.rs | 90 +++ crates/jmap/src/identity/mod.rs | 2 + crates/jmap/src/identity/set.rs | 273 ++++++++++ crates/jmap/src/lib.rs | 4 +- crates/jmap/src/mailbox/get.rs | 2 +- crates/jmap/src/mailbox/set.rs | 16 +- crates/jmap/src/services/ingest.rs | 6 +- crates/jmap/src/sieve/get.rs | 2 +- crates/jmap/src/sieve/ingest.rs | 12 +- crates/jmap/src/sieve/set.rs | 35 +- crates/jmap/src/submission/get.rs | 167 ++++++ crates/jmap/src/submission/mod.rs | 3 + crates/jmap/src/submission/query.rs | 92 ++++ crates/jmap/src/submission/set.rs | 695 ++++++++++++++++++++++++ crates/jmap/src/thread/get.rs | 2 +- crates/jmap/src/vacation/get.rs | 2 +- crates/jmap/src/vacation/set.rs | 6 +- crates/smtp/src/core/mod.rs | 124 +++-- crates/smtp/src/inbound/data.rs | 4 +- crates/smtp/src/inbound/session.rs | 2 +- crates/store/src/write/assert.rs | 2 +- crates/utils/src/listener/limiter.rs | 27 +- tests/src/jmap/auth_limits.rs | 31 +- tests/src/jmap/email_submission.rs | 316 +++++------ tests/src/jmap/mod.rs | 18 +- tests/src/jmap/sieve_script.rs | 4 - tests/src/jmap/vacation_response.rs | 2 - 43 files changed, 2065 insertions(+), 611 deletions(-) create mode 100644 crates/jmap/src/identity/get.rs create mode 100644 crates/jmap/src/identity/mod.rs create mode 100644 crates/jmap/src/identity/set.rs create mode 100644 crates/jmap/src/submission/get.rs create mode 100644 crates/jmap/src/submission/mod.rs create mode 100644 crates/jmap/src/submission/query.rs create mode 100644 crates/jmap/src/submission/set.rs diff --git a/Cargo.lock b/Cargo.lock index 93bed1ff..4ef377da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,18 +18,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if", - "cipher 0.3.0", - "cpufeatures", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.2" @@ -37,19 +25,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] [[package]] name = "aes-gcm" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" dependencies = [ "aead", - "aes 0.8.2", - "cipher 0.4.4", + "aes", + "cipher", "ctr", "ghash", "subtle", @@ -62,8 +50,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" dependencies = [ "aead", - "aes 0.8.2", - "cipher 0.4.4", + "aes", + "cipher", "ctr", "polyval", "subtle", @@ -96,9 +84,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -143,7 +131,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -177,7 +165,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -199,7 +187,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -210,7 +198,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -304,9 +292,9 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" [[package]] name = "base64ct" @@ -374,9 +362,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70beb79cbb5ce9c4f8e20849978f34225931f665bb49efa6982875a4d5facb3" +checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" [[package]] name = "bitpacking" @@ -412,9 +400,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "bytemuck" @@ -504,15 +492,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -536,9 +515,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", @@ -558,16 +537,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "const-oid" version = "0.9.2" @@ -604,9 +573,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -744,51 +713,7 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.15", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", + "cipher", ] [[package]] @@ -798,7 +723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core", @@ -806,9 +731,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "der" @@ -823,9 +748,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e58dffcdcc8ee7b22f0c1f71a69243d7c2d9ad87b5a14361f2424a1565c219" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" dependencies = [ "const-oid", "pem-rfc7468 0.7.0", @@ -848,9 +773,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", @@ -866,7 +791,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -881,7 +806,7 @@ version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" dependencies = [ - "der 0.7.5", + "der 0.7.6", "digest", "elliptic-curve", "rfc6979", @@ -918,9 +843,9 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c71eaa367f2e5d556414a8eea812bc62985c879748d6403edabd9cb03f16e7" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" dependencies = [ "base16ct", "crypto-bigint", @@ -1060,9 +985,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "libz-sys", @@ -1240,7 +1165,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -1295,12 +1220,12 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199523ba70af2b447640715e8c4bd2b5360313a71d2d69361ae4dd1dc31487dd" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows", + "windows-targets 0.48.0", ] [[package]] @@ -1343,9 +1268,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", @@ -1370,12 +1295,21 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.8.1" +name = "hashbrown" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "hashbrown", + "ahash 0.8.3", +] + +[[package]] +name = "hashlink" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" +dependencies = [ + "hashbrown 0.13.2", ] [[package]] @@ -1566,15 +1500,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" dependencies = [ "http", "hyper 0.14.26", - "rustls 0.20.8", + "rustls 0.21.1", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.0", ] [[package]] @@ -1605,12 +1539,11 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1641,7 +1574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -1714,7 +1647,7 @@ checksum = "37228e06c75842d1097432d94d02f37fe3ebfca9791c2e8fef6e9db17ed128c1" dependencies = [ "cedarwood", "fxhash", - "hashbrown", + "hashbrown 0.12.3", "lazy_static", "phf", "phf_codegen", @@ -1728,7 +1661,7 @@ dependencies = [ "aes-gcm", "aes-gcm-siv", "async-stream", - "base64 0.21.0", + "base64 0.21.1", "bincode", "ece", "form-data", @@ -1749,6 +1682,7 @@ dependencies = [ "sha2", "sieve-rs", "smtp", + "smtp-proto", "sqlx", "store", "tokio", @@ -1799,9 +1733,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -1823,9 +1757,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libloading" @@ -1839,9 +1773,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "librocksdb-sys" @@ -1872,24 +1806,15 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1898,9 +1823,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" @@ -1971,7 +1896,7 @@ dependencies = [ [[package]] name = "mail-parser" version = "0.8.2" -source = "git+https://github.com/stalwartlabs/mail-parser#c7790a89256510f0f09382b73bff6ddcef5f0a5c" +source = "git+https://github.com/stalwartlabs/mail-parser#154591697f7ac92326fa7c532e1827e87ae8d30d" dependencies = [ "encoding_rs", "serde", @@ -1988,7 +1913,7 @@ dependencies = [ "mail-builder", "md5", "rand", - "rustls 0.21.0", + "rustls 0.21.1", "smtp-proto", "tokio", "tokio-rustls 0.24.0", @@ -2114,9 +2039,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] @@ -2286,7 +2211,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -2581,22 +2506,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] @@ -2639,15 +2564,15 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.5", + "der 0.7.6", "spki 0.7.2", ] [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "polyval" @@ -2698,9 +2623,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" dependencies = [ "unicode-ident", ] @@ -2776,9 +2701,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -2866,13 +2791,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.1", ] [[package]] @@ -2881,7 +2806,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.29", ] [[package]] @@ -2891,12 +2816,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "reqwest" -version = "0.11.16" +name = "regex-syntax" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "base64 0.21.1", "bytes", "encoding_rs", "futures-core", @@ -2913,14 +2844,14 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.8", + "rustls 0.21.1", "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.0", "tokio-util", "tower-service", "url", @@ -3020,7 +2951,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.1.0", + "bitflags 2.3.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3055,9 +2986,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.13" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags 1.3.2", "errno", @@ -3081,9 +3012,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" dependencies = [ "log", "ring", @@ -3109,7 +3040,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64 0.21.1", ] [[package]] @@ -3158,12 +3089,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - [[package]] name = "sct" version = "0.7.0" @@ -3181,7 +3106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" dependencies = [ "base16ct", - "der 0.7.5", + "der 0.7.6", "generic-array", "pkcs8 0.10.2", "subtle", @@ -3190,9 +3115,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3213,22 +3138,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -3276,7 +3201,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -3319,7 +3244,7 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "sieve-rs" version = "0.3.0" -source = "git+https://github.com/stalwartlabs/sieve#664a542e07865291f41770bebbe9407d179a5233" +source = "git+https://github.com/stalwartlabs/sieve#83f1f9cead81625a8f512434ef7b701588754ea2" dependencies = [ "ahash 0.8.3", "bincode", @@ -3392,7 +3317,7 @@ dependencies = [ "rayon", "regex", "reqwest", - "rustls 0.21.0", + "rustls 0.21.1", "rustls-pemfile", "serde", "serde_json", @@ -3412,7 +3337,7 @@ dependencies = [ [[package]] name = "smtp-proto" version = "0.1.1" -source = "git+https://github.com/stalwartlabs/smtp-proto#72fd0e2da05c2bf45770a58c86833785220d7f19" +source = "git+https://github.com/stalwartlabs/smtp-proto#434ede72159b5f9ea588b9ee8c361a3247aa2f35" [[package]] name = "socket2" @@ -3456,7 +3381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" dependencies = [ "base64ct", - "der 0.7.5", + "der 0.7.6", ] [[package]] @@ -3512,7 +3437,7 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls 0.21.0", + "rustls 0.21.1", "rustls-pemfile", "serde", "serde_json", @@ -3573,7 +3498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c430536df19e8b5b048a9ae19b266aba77f9f3e2255b7195f465d678cb2d0a" dependencies = [ "atoi", - "base64 0.21.0", + "base64 0.21.1", "bitflags 1.3.2", "byteorder", "bytes", @@ -3615,7 +3540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "210e0a1523b6d46ca73db1c5197a233a8e14787596910ce88ff5d47a00da0241" dependencies = [ "atoi", - "base64 0.21.0", + "base64 0.21.1", "bitflags 1.3.2", "byteorder", "crc", @@ -3725,9 +3650,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -3742,9 +3667,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" dependencies = [ "proc-macro2", "quote", @@ -3797,7 +3722,7 @@ version = "0.1.0" dependencies = [ "ahash 0.8.3", "async-trait", - "base64 0.21.0", + "base64 0.21.1", "bytes", "chrono", "csv", @@ -3816,7 +3741,7 @@ dependencies = [ "num_cpus", "rayon", "reqwest", - "rustls 0.21.0", + "rustls 0.21.1", "rustls-pemfile", "serde", "serde_json", @@ -3856,7 +3781,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -3882,9 +3807,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -3894,15 +3819,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -3934,9 +3859,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", @@ -3948,7 +3873,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -3963,13 +3888,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -3989,7 +3914,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" dependencies = [ - "rustls 0.21.0", + "rustls 0.21.1", "tokio", ] @@ -4022,9 +3947,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -4131,26 +4056,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ "crossbeam-channel", - "time 0.3.20", + "time 0.3.21", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -4193,9 +4118,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", @@ -4322,12 +4247,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "unicode-xid" version = "0.2.4" @@ -4342,9 +4261,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "universal-hash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle", @@ -4383,7 +4302,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "privdrop", - "rustls 0.21.0", + "rustls 0.21.1", "rustls-pemfile", "serde", "smtp-proto", @@ -4397,9 +4316,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" [[package]] name = "valuable" @@ -4443,9 +4362,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4453,24 +4372,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", @@ -4480,9 +4399,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4490,22 +4409,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "wasm-streams" @@ -4522,9 +4441,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", @@ -4564,7 +4483,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c531a2dc4c462b833788be2c07eef4e621d0e9edbd55bf280cc164c1c1aa043" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", "once_cell", ] @@ -4801,14 +4720,14 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "1690519550bfa95525229b9ca2350c63043a4857b3b0013811b2ccf4a2420b01" [[package]] name = "xxhash-rust" @@ -4824,11 +4743,11 @@ checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zip" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes 0.7.5", + "aes", "byteorder", "bzip2", "constant_time_eq 0.1.5", @@ -4838,7 +4757,7 @@ dependencies = [ "hmac", "pbkdf2", "sha1", - "time 0.3.20", + "time 0.3.21", "zstd", ] diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs index c387ce2f..d2bee1e7 100644 --- a/crates/jmap-proto/src/method/set.rs +++ b/crates/jmap-proto/src/method/set.rs @@ -322,6 +322,10 @@ impl JsonObjectParser for Object { parser.next_token()?, parser, )?), + Property::Parameters => SetValue::Value(Value::parse::( + parser.next_token()?, + parser, + )?), Property::Members => SetValue::Value(Value::parse::( parser.next_token()?, parser, diff --git a/crates/jmap-proto/src/object/index.rs b/crates/jmap-proto/src/object/index.rs index e32fad07..171b9c13 100644 --- a/crates/jmap-proto/src/object/index.rs +++ b/crates/jmap-proto/src/object/index.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashSet}; use store::{ fts::builder::ToTokens, - write::{BatchBuilder, BitmapFamily, IntoOperations, Operation}, + write::{assert::HashedValue, BatchBuilder, BitmapFamily, IntoOperations, Operation}, Serialize, HASH_EXACT, }; @@ -16,7 +16,7 @@ use super::Object; #[derive(Debug, Clone, Default)] pub struct ObjectIndexBuilder { index: &'static [IndexProperty], - current: Option>, + current: Option>>, changes: Option>, } @@ -56,7 +56,7 @@ impl ObjectIndexBuilder { } } - pub fn with_current(mut self, current: Object) -> Self { + pub fn with_current(mut self, current: HashedValue>) -> Self { self.current = Some(current); self } @@ -66,7 +66,7 @@ impl ObjectIndexBuilder { self } - pub fn with_current_opt(mut self, current: Option>) -> Self { + pub fn with_current_opt(mut self, current: Option>>) -> Self { self.current = current; self } @@ -78,7 +78,7 @@ impl ObjectIndexBuilder { .or_else(|| { self.current .as_ref() - .and_then(|c| c.properties.get(property)) + .and_then(|c| c.inner.properties.get(property)) }) .unwrap_or(&Value::Null) } @@ -130,11 +130,13 @@ impl IntoOperations for ObjectIndexBuilder { } (Some(current), Some(changes)) => { // Update - merge_batch(batch, self.index, current, changes); + batch.assert_value(Property::Value, ¤t); + merge_batch(batch, self.index, current.inner, changes); } (Some(current), None) => { // Deletion - build_batch(batch, self.index, ¤t, false); + batch.assert_value(Property::Value, ¤t); + build_batch(batch, self.index, ¤t.inner, false); batch.ops.push(Operation::Value { field: Property::Value.into(), family: 0, diff --git a/crates/jmap-proto/src/types/property.rs b/crates/jmap-proto/src/types/property.rs index 484bd8a9..1d92e02a 100644 --- a/crates/jmap-proto/src/types/property.rs +++ b/crates/jmap-proto/src/types/property.rs @@ -993,7 +993,110 @@ impl From for Property { impl SerializeInto for Property { fn serialize_into(&self, buf: &mut Vec) { - buf.push(self.into()); + buf.push(match self { + Property::IsActive => 0, + Property::IsEnabled => 1, + Property::IsSubscribed => 2, + Property::Keys => 3, + Property::Keywords => 4, + Property::Language => 5, + Property::Location => 6, + Property::MailboxIds => 7, + Property::MayDelete => 8, + Property::MdnBlobIds => 9, + Property::Members => 10, + Property::MessageId => 11, + Property::MyRights => 12, + Property::Name => 13, + Property::ParentId => 14, + Property::PartId => 15, + Property::Picture => 16, + Property::Preview => 17, + Property::Quota => 18, + Property::ReceivedAt => 19, + Property::References => 20, + Property::ReplyTo => 21, + Property::Role => 22, + Property::Secret => 23, + Property::SendAt => 24, + Property::Sender => 25, + Property::SentAt => 26, + Property::Size => 27, + Property::SortOrder => 28, + Property::Subject => 29, + Property::SubParts => 30, + Property::TextBody => 31, + Property::TextSignature => 32, + Property::ThreadId => 33, + Property::Timezone => 34, + Property::To => 35, + Property::ToDate => 36, + Property::TotalEmails => 37, + Property::TotalThreads => 38, + Property::Type => 39, + Property::Types => 40, + Property::UndoStatus => 41, + Property::UnreadEmails => 42, + Property::UnreadThreads => 43, + Property::Url => 44, + Property::VerificationCode => 45, + Property::Parameters => 46, + Property::Addresses => 47, + Property::P256dh => 48, + Property::Auth => 49, + Property::Value => 50, + Property::SmtpReply => 51, + Property::Delivered => 52, + Property::Displayed => 53, + Property::MailFrom => 54, + Property::RcptTo => 55, + Property::IsEncodingProblem => 56, + Property::IsTruncated => 57, + Property::MayReadItems => 58, + Property::MayAddItems => 59, + Property::MayRemoveItems => 60, + Property::MaySetSeen => 61, + Property::MaySetKeywords => 62, + Property::MayCreateChild => 63, + Property::MayRename => 64, + Property::MaySubmit => 65, + Property::Acl => 66, + Property::Aliases => 67, + Property::Attachments => 68, + Property::Bcc => 69, + Property::BlobId => 70, + Property::BodyStructure => 71, + Property::BodyValues => 72, + Property::Capabilities => 73, + Property::Cc => 74, + Property::Charset => 75, + Property::Cid => 76, + Property::DeliveryStatus => 77, + Property::Description => 78, + Property::DeviceClientId => 79, + Property::Disposition => 80, + Property::DsnBlobIds => 81, + Property::Email => 82, + Property::EmailId => 83, + Property::EmailIds => 84, + Property::Envelope => 85, + Property::Expires => 86, + Property::From => 87, + Property::FromDate => 88, + Property::HasAttachment => 89, + Property::Header(_) => 90, + Property::Headers => 91, + Property::HtmlBody => 92, + Property::HtmlSignature => 93, + Property::Id => 94, + Property::IdentityId => 95, + Property::InReplyTo => 96, + Property::_T(value) => { + buf.push(97); + value.serialize_into(buf); + return; + } + }); } } @@ -1101,7 +1204,7 @@ impl DeserializeFrom for Property { 94 => Some(Property::Id), 95 => Some(Property::IdentityId), 96 => Some(Property::InReplyTo), - 97 => Some(Property::_T(String::new())), // Never serialized + 97 => String::deserialize_from(bytes).map(Property::_T), _ => None, } } diff --git a/crates/jmap-proto/src/types/value.rs b/crates/jmap-proto/src/types/value.rs index 460d60d4..4ed8e1fc 100644 --- a/crates/jmap-proto/src/types/value.rs +++ b/crates/jmap-proto/src/types/value.rs @@ -280,6 +280,20 @@ impl Value { } } + pub fn as_obj(&self) -> Option<&Object> { + match self { + Value::Object(o) => Some(o), + _ => None, + } + } + + pub fn as_obj_mut(&mut self) -> Option<&mut Object> { + match self { + Value::Object(o) => Some(o), + _ => None, + } + } + pub fn try_cast_uint(&self) -> Option { match self { Value::UnsignedInt(u) => Some(*u), @@ -411,6 +425,12 @@ impl From for Value { } } +impl From for Value { + fn from(date: UTCDate) -> Self { + Value::Date(date) + } +} + impl From> for Value { fn from(value: Cow<'_, str>) -> Self { Value::Text(value.into_owned()) diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 2450163a..a9903027 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -9,6 +9,7 @@ store = { path = "../store" } jmap_proto = { path = "../jmap-proto" } utils = { path = "../utils" } smtp = { path = "../smtp" } +smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" } mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] } mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] } mail-send = { git = "https://github.com/stalwartlabs/mail-send" } diff --git a/crates/jmap/src/api/config.rs b/crates/jmap/src/api/config.rs index 0dfcb7d6..a3d97850 100644 --- a/crates/jmap/src/api/config.rs +++ b/crates/jmap/src/api/config.rs @@ -65,11 +65,11 @@ impl crate::Config { .property("jmap.session.cache.ttl")? .unwrap_or(Duration::from_secs(3600)), rate_authenticated: settings - .property_or_static("jmap.rate-limit.account.rate", "1000/1s")?, + .property_or_static("jmap.rate-limit.account.rate", "1000/1m")?, rate_authenticate_req: settings - .property_or_static("jmap.rate-limit.authentication.rate", "10/1s")?, + .property_or_static("jmap.rate-limit.authentication.rate", "10/1m")?, rate_anonymous: settings - .property_or_static("jmap.rate-limit.anonymous.rate", "100/1s")?, + .property_or_static("jmap.rate-limit.anonymous.rate", "100/1m")?, rate_use_forwarded: settings .property("jmap.rate-limit.use-forwarded")? .unwrap_or(false), diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 7d458bc1..f13991f9 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -33,7 +33,7 @@ impl JMAP { &self, req: &mut hyper::Request, remote_ip: IpAddr, - instance: &ServerInstance, + instance: &Arc, ) -> HttpResponse { let mut path = req.uri().path().split('/'); path.next(); @@ -55,7 +55,7 @@ impl JMAP { //let delete = "fd"; //println!("<- {}", String::from_utf8_lossy(&bytes)); - match self.handle_request(&bytes, acl_token).await { + match self.handle_request(&bytes, acl_token, instance).await { Ok(response) => response.into_http_response(), Err(err) => err.into_http_response(), } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 13e82b32..faa0ca54 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -10,6 +10,7 @@ use jmap_proto::{ response::{Response, ResponseMethod}, types::collection::Collection, }; +use utils::listener::ServerInstance; use crate::{auth::AclToken, JMAP}; @@ -18,6 +19,7 @@ impl JMAP { &self, bytes: &[u8], acl_token: Arc, + instance: &Arc, ) -> Result { let request = Request::parse( bytes, @@ -43,7 +45,7 @@ impl JMAP { // Add response match self - .handle_method_call(call.method, &acl_token, &mut next_call) + .handle_method_call(call.method, &acl_token, &mut next_call, instance) .await { Ok(mut method_response) => { @@ -104,6 +106,7 @@ impl JMAP { method: RequestMethod, acl_token: &AclToken, next_call: &mut Option>, + instance: &Arc, ) -> Result { Ok(match method { RequestMethod::Get(mut req) => match req.take_arguments() { @@ -124,8 +127,16 @@ impl JMAP { self.thread_get(req).await?.into() } - get::RequestArguments::Identity => todo!(), - get::RequestArguments::EmailSubmission => todo!(), + get::RequestArguments::Identity => { + acl_token.assert_is_member(req.account_id)?; + + self.identity_get(req).await?.into() + } + get::RequestArguments::EmailSubmission => { + acl_token.assert_is_member(req.account_id)?; + + self.email_submission_get(req).await?.into() + } get::RequestArguments::PushSubscription => { self.push_subscription_get(req, acl_token).await?.into() } @@ -155,7 +166,11 @@ impl JMAP { .await? .into() } - query::RequestArguments::EmailSubmission => todo!(), + query::RequestArguments::EmailSubmission => { + acl_token.assert_is_member(req.account_id)?; + + self.email_submission_query(req).await?.into() + } query::RequestArguments::SieveScript => { acl_token.assert_is_member(req.account_id)?; @@ -175,8 +190,18 @@ impl JMAP { .await? .into() } - set::RequestArguments::Identity => todo!(), - set::RequestArguments::EmailSubmission(_) => todo!(), + set::RequestArguments::Identity => { + acl_token.assert_is_member(req.account_id)?; + + self.identity_set(req).await?.into() + } + set::RequestArguments::EmailSubmission(arguments) => { + acl_token.assert_is_member(req.account_id)?; + + self.email_submission_set(req.with_arguments(arguments), instance, next_call) + .await? + .into() + } set::RequestArguments::PushSubscription => { self.push_subscription_set(req, acl_token).await?.into() } diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 9f6ab56d..72eee76f 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -193,6 +193,19 @@ impl crate::Config { Capability::Mail, Capabilities::Mail(MailCapabilities::new(self)), ); + self.capabilities.capabilities.append( + Capability::Submission, + Capabilities::Submission(SubmissionCapabilities { + max_delayed_send: 86400 * 30, + submission_extensions: vec![ + "FUTURERELEASE".to_string(), + "SIZE".to_string(), + "DSN".to_string(), + "DELIVERYBY".to_string(), + "MT-PRIORITY".to_string(), + ], + }), + ); self.capabilities.capabilities.append( Capability::Sieve, Capabilities::Sieve(SieveCapabilities::new(self, settings)), diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 5680b454..5acb2c06 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -9,7 +9,11 @@ use jmap_proto::{ value::{MaybePatchValue, Value}, }, }; -use store::{roaring::RoaringBitmap, write::key::DeserializeBigEndian, AclKey, Deserialize, Error}; +use store::{ + roaring::RoaringBitmap, + write::{assert::HashedValue, key::DeserializeBigEndian}, + AclKey, Deserialize, Error, +}; use utils::map::bitmap::{Bitmap, BitmapItem}; use crate::{JMAP, SUPERUSER_ID}; @@ -283,7 +287,7 @@ impl JMAP { pub async fn acl_set( &self, changes: &mut Object, - current: Option<&Object>, + current: Option<&HashedValue>>, acl_changes: MaybePatchValue, ) -> Result<(), SetError> { match acl_changes { @@ -300,7 +304,9 @@ impl JMAP { .properties .get_mut_or_insert_with(Property::Acl, || { current - .and_then(|current| current.properties.get(&Property::Acl).cloned()) + .and_then(|current| { + current.inner.properties.get(&Property::Acl).cloned() + }) .unwrap_or_else(|| Value::List(Vec::new())) }) { acl @@ -403,12 +409,16 @@ impl JMAP { } } - pub fn refresh_acls(&self, changes: &Object, current: &Option>) { + pub fn refresh_acls( + &self, + changes: &Object, + current: &Option>>, + ) { if let Value::List(acl_changes) = changes.get(&Property::Acl) { let mut acl_tokens = self.acl_tokens.lock(); if let Some(Value::List(acl_current)) = current .as_ref() - .and_then(|current| current.properties.get(&Property::Acl)) + .and_then(|current| current.inner.properties.get(&Property::Acl)) { for current_item in acl_current.chunks_exact(2) { let mut invalidate = true; diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index 7cfd4501..32372e42 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -23,6 +23,7 @@ pub struct AuthenticatedLimiter { concurrent_uploads: ConcurrencyLimiter, } +#[derive(Debug)] pub struct AnonymousLimiter { request_limiter: RateLimiter, auth_limiter: RateLimiter, diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index 32c6f914..46954de5 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -55,7 +55,7 @@ impl JMAP { anchor_offset: None, limit: None, calculate_total: request.calculate_total, - arguments: (), + arguments: query::RequestArguments::EmailSubmission, }; let is_mutable = query.filter.iter().any(|f| !f.is_immutable()) || query @@ -72,8 +72,7 @@ impl JMAP { .await? } query::RequestArguments::EmailSubmission => { - let implement = "true"; - todo!() + self.email_submission_query(query).await? } _ => unreachable!(), }; diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs index ac7deab0..cd36a28d 100644 --- a/crates/jmap/src/email/get.rs +++ b/crates/jmap/src/email/get.rs @@ -89,7 +89,7 @@ impl JMAP { .collect() }; let mut response = GetResponse { - account_id: Some(request.account_id), + account_id: request.account_id.into(), state: self.get_state(account_id, Collection::Email).await?.into(), list: Vec::with_capacity(ids.len()), not_found: vec![], diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index 48d5f807..62fd8175 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -14,7 +14,7 @@ use jmap_proto::{ }; use utils::map::vec_map::VecMap; -use crate::{auth::AclToken, MaybeError, JMAP}; +use crate::{auth::AclToken, IngestError, JMAP}; impl JMAP { pub async fn email_import( @@ -111,13 +111,13 @@ impl JMAP { Ok(email) => { response.created.append(id, email.into()); } - Err(MaybeError::Permanent { reason, .. }) => { + Err(IngestError::Permanent { reason, .. }) => { response.not_created.append( id, SetError::new(SetErrorType::InvalidEmail).with_description(reason), ); } - Err(MaybeError::Temporary) => { + Err(IngestError::Temporary) => { return Err(MethodError::ServerPartialFail); } } diff --git a/crates/jmap/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs index 4808a723..cee047f9 100644 --- a/crates/jmap/src/email/ingest.rs +++ b/crates/jmap/src/email/ingest.rs @@ -18,7 +18,7 @@ use utils::map::vec_map::VecMap; use crate::{ email::index::{IndexMessage, MAX_ID_LENGTH}, - MaybeError, JMAP, + IngestError, JMAP, }; use super::index::{TrimTextValue, MAX_SORT_FIELD_LENGTH}; @@ -46,10 +46,10 @@ impl JMAP { keywords: Vec, received_at: Option, skip_duplicates: bool, - ) -> Result { + ) -> Result { // Parse message let raw_message = ingest_email.raw_message; - let message = ingest_email.message.ok_or_else(|| MaybeError::Permanent { + let message = ingest_email.message.ok_or_else(|| IngestError::Permanent { code: [5, 5, 0], reason: "Failed to parse e-mail message.".to_string(), })?; @@ -114,7 +114,7 @@ impl JMAP { context = "find_duplicates", error = ?err, "Duplicate message search failed."); - MaybeError::Temporary + IngestError::Temporary })? .results .is_empty() @@ -145,7 +145,7 @@ impl JMAP { context = "email_ingest", error = ?err, "Failed to assign documentId."); - MaybeError::Temporary + IngestError::Temporary })?; let change_id = self .store @@ -157,7 +157,7 @@ impl JMAP { context = "email_ingest", error = ?err, "Failed to assign changeId."); - MaybeError::Temporary + IngestError::Temporary })?; // Store blob @@ -171,7 +171,7 @@ impl JMAP { context = "email_ingest", error = ?err, "Failed to write blob."); - MaybeError::Temporary + IngestError::Temporary })?; // Build change log @@ -190,7 +190,7 @@ impl JMAP { context = "email_ingest", error = ?err, "Failed to assign documentId for new thread."); - MaybeError::Temporary + IngestError::Temporary })?; changes.log_insert(Collection::Thread, thread_id); thread_id @@ -220,7 +220,7 @@ impl JMAP { context = "email_ingest", error = ?err, "Failed to index message."); - MaybeError::Temporary + IngestError::Temporary })? .value(Property::ThreadId, thread_id, F_VALUE | F_BITMAP) .custom(changes); @@ -230,7 +230,7 @@ impl JMAP { context = "email_ingest", error = ?err, "Failed to write message to database."); - MaybeError::Temporary + IngestError::Temporary })?; Ok(IngestedEmail { @@ -246,7 +246,7 @@ impl JMAP { account_id: u32, thread_name: &str, references: &[&str], - ) -> Result, MaybeError> { + ) -> Result, IngestError> { let mut try_count = 0; loop { @@ -268,7 +268,7 @@ impl JMAP { context = "find_or_merge_thread", error = ?err, "Thread search failed."); - MaybeError::Temporary + IngestError::Temporary })? .results; @@ -299,7 +299,7 @@ impl JMAP { context = "find_or_merge_thread", error = ?err, "Failed to obtain threadIds."); - MaybeError::Temporary + IngestError::Temporary })?; if thread_ids.len() == 1 { @@ -337,7 +337,7 @@ impl JMAP { context = "find_or_merge_thread", error = ?err, "Failed to assign changeId for thread merge."); - MaybeError::Temporary + IngestError::Temporary })?; let mut changes = ChangeLogBuilder::with_change_id(change_id); batch @@ -369,7 +369,7 @@ impl JMAP { context = "find_or_merge_thread", error = ?err, "Failed to obtain threadId bitmap."); - MaybeError::Temporary + IngestError::Temporary })? .unwrap_or_default() { @@ -399,7 +399,7 @@ impl JMAP { context = "find_or_merge_thread", error = ?err, "Failed to write thread merge batch."); - return Err(MaybeError::Temporary); + return Err(IngestError::Temporary); } } } diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs new file mode 100644 index 00000000..e432e1e5 --- /dev/null +++ b/crates/jmap/src/identity/get.rs @@ -0,0 +1,90 @@ +use jmap_proto::{ + error::method::MethodError, + method::get::{GetRequest, GetResponse, RequestArguments}, + object::Object, + types::{collection::Collection, property::Property, value::Value}, +}; + +use crate::JMAP; + +impl JMAP { + pub async fn identity_get( + &self, + mut request: GetRequest, + ) -> Result { + let ids = request.unwrap_ids(self.config.get_max_objects)?; + let properties = request.unwrap_properties(&[ + Property::Id, + Property::Name, + Property::Email, + Property::ReplyTo, + Property::Bcc, + Property::TextSignature, + Property::HtmlSignature, + Property::MayDelete, + ]); + let account_id = request.account_id.document_id(); + let identity_ids = self + .get_document_ids(account_id, Collection::Identity) + .await? + .unwrap_or_default(); + let ids = if let Some(ids) = ids { + ids + } else { + identity_ids + .iter() + .take(self.config.get_max_objects) + .map(Into::into) + .collect::>() + }; + let mut response = GetResponse { + account_id: request.account_id.into(), + state: self + .get_state(account_id, Collection::Identity) + .await? + .into(), + list: Vec::with_capacity(ids.len()), + not_found: vec![], + }; + + for id in ids { + // Obtain the identity object + let document_id = id.document_id(); + if !identity_ids.contains(document_id) { + response.not_found.push(id); + continue; + } + let mut push = if let Some(push) = self + .get_property::>( + account_id, + Collection::Identity, + document_id, + Property::Value, + ) + .await? + { + push + } else { + response.not_found.push(id); + continue; + }; + let mut result = Object::with_capacity(properties.len()); + for property in &properties { + match property { + Property::Id => { + result.append(Property::Id, Value::Id(id)); + } + Property::MayDelete => { + result.append(Property::MayDelete, Value::Bool(true)); + } + property => { + result.append(property.clone(), push.remove(property)); + } + } + } + response.list.push(result); + } + + Ok(response) + } +} diff --git a/crates/jmap/src/identity/mod.rs b/crates/jmap/src/identity/mod.rs new file mode 100644 index 00000000..e7537b1c --- /dev/null +++ b/crates/jmap/src/identity/mod.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod set; diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs new file mode 100644 index 00000000..3ccb854a --- /dev/null +++ b/crates/jmap/src/identity/set.rs @@ -0,0 +1,273 @@ +use jmap_proto::{ + error::{method::MethodError, set::SetError}, + method::set::{RequestArguments, SetRequest, SetResponse}, + object::Object, + response::references::EvalObjectReferences, + types::{ + collection::Collection, + property::Property, + value::{MaybePatchValue, Value}, + }, +}; +use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE}; + +use crate::JMAP; + +impl JMAP { + pub async fn identity_set( + &self, + mut request: SetRequest, + ) -> Result { + let account_id = request.account_id.document_id(); + let mut identity_ids = self + .get_document_ids(account_id, Collection::Identity) + .await? + .unwrap_or_default(); + let mut response = SetResponse::from_request(&request, self.config.set_max_objects)?; + let will_destroy = request.unwrap_destroy(); + + // Process creates + let mut changes = ChangeLogBuilder::new(); + 'create: for (id, object) in request.unwrap_create() { + let mut identity = Object::with_capacity(object.properties.len()); + + for (property, value) in object.properties { + match response + .eval_object_references(value) + .and_then(|value| validate_identity_value(&property, value, None)) + { + Ok(Value::Null) => (), + Ok(value) => { + identity.set(property, value); + } + Err(err) => { + response.not_created.append(id, err); + continue 'create; + } + } + } + + // Validate email address + if let Value::Text(email) = identity.get(&Property::Email) { + if !self.get_addresses_by_uid(account_id).await.contains(email) { + response.not_created.append( + id, + SetError::invalid_properties() + .with_property(Property::Email) + .with_description( + "E-mail address not configured for this account.".to_string(), + ), + ); + continue 'create; + } + } else { + response.not_created.append( + id, + SetError::invalid_properties() + .with_property(Property::Email) + .with_description("Missing e-mail address."), + ); + continue 'create; + } + + // Insert record + let mut batch = BatchBuilder::new(); + let document_id = self + .assign_document_id(account_id, Collection::Identity) + .await?; + batch + .with_account_id(account_id) + .with_collection(Collection::Identity) + .create_document(document_id) + .value(Property::Value, identity, F_VALUE); + identity_ids.insert(document_id); + self.write_batch(batch).await?; + changes.log_insert(Collection::Identity, document_id); + response.created(id, document_id); + } + + // Process updates + 'update: for (id, object) in request.unwrap_update() { + // Make sure id won't be destroyed + if will_destroy.contains(&id) { + response.not_updated.append(id, SetError::will_destroy()); + continue 'update; + } + + // Obtain identity + let document_id = id.document_id(); + let mut identity = if let Some(identity) = self + .get_property::>( + account_id, + Collection::Identity, + document_id, + Property::Value, + ) + .await? + { + identity + } else { + response.not_updated.append(id, SetError::not_found()); + continue 'update; + }; + + for (property, value) in object.properties { + match response + .eval_object_references(value) + .and_then(|value| validate_identity_value(&property, value, Some(&identity))) + { + Ok(Value::Null) => { + identity.remove(&property); + } + Ok(value) => { + identity.set(property, value); + } + Err(err) => { + response.not_updated.append(id, err); + continue 'update; + } + }; + } + + // Update record + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Identity) + .update_document(document_id) + .value(Property::Value, identity, F_VALUE); + self.write_batch(batch).await?; + changes.log_update(Collection::Identity, document_id); + response.updated.append(id, None); + } + + // Process deletions + for id in will_destroy { + let document_id = id.document_id(); + if identity_ids.contains(document_id) { + // Update record + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Identity) + .delete_document(document_id) + .value(Property::Value, (), F_VALUE | F_CLEAR); + self.write_batch(batch).await?; + changes.log_delete(Collection::Identity, document_id); + response.destroyed.push(id); + } else { + response.not_destroyed.append(id, SetError::not_found()); + } + } + + // Write changes + if !changes.is_empty() { + response.new_state = self.commit_changes(account_id, changes).await?.into(); + } + + Ok(response) + } +} + +fn validate_identity_value( + property: &Property, + value: MaybePatchValue, + current: Option<&Object>, +) -> Result { + Ok(match (property, value) { + (Property::Name, MaybePatchValue::Value(Value::Text(value))) if value.len() < 255 => { + Value::Text(value) + } + (Property::Email, MaybePatchValue::Value(Value::Text(value))) + if current.is_none() && value.len() < 255 => + { + Value::Text(sanitize_email(&value).ok_or_else(|| { + SetError::invalid_properties() + .with_property(Property::Email) + .with_description("Invalid e-mail address.") + })?) + } + ( + Property::TextSignature | Property::HtmlSignature, + MaybePatchValue::Value(Value::Text(value)), + ) if value.len() < 2048 => Value::Text(value), + (Property::ReplyTo | Property::Bcc, MaybePatchValue::Value(Value::List(value))) => { + for addr in &value { + let mut is_valid = false; + if let Value::Object(obj) = addr { + for (key, value) in &obj.properties { + match (key, value) { + (Property::Email, Value::Text(value)) if value.len() < 255 => { + is_valid = true + } + (Property::Name, Value::Text(value)) if value.len() < 255 => (), + (Property::Name, Value::Null) => (), + _ => { + is_valid = false; + break; + } + } + } + } + + if !is_valid { + return Err(SetError::invalid_properties() + .with_property(property.clone()) + .with_description("Invalid e-mail address object.")); + } + } + + Value::List(value) + } + ( + Property::Name + | Property::TextSignature + | Property::HtmlSignature + | Property::ReplyTo + | Property::Bcc, + MaybePatchValue::Value(Value::Null), + ) => Value::Null, + + (property, _) => { + return Err(SetError::invalid_properties() + .with_property(property.clone()) + .with_description("Field could not be set.")); + } + }) +} + +// Basic email sanitizer +pub fn sanitize_email(email: &str) -> Option { + let mut result = String::with_capacity(email.len()); + let mut found_local = false; + let mut found_domain = false; + let mut last_ch = char::from(0); + + for ch in email.chars() { + if !ch.is_whitespace() { + if ch == '@' { + if !result.is_empty() && !found_local { + found_local = true; + } else { + return None; + } + } else if ch == '.' { + if !(last_ch.is_alphanumeric() || last_ch == '-' || last_ch == '_') { + return None; + } else if found_local { + found_domain = true; + } + } + last_ch = ch; + for ch in ch.to_lowercase() { + result.push(ch); + } + } + } + + if found_domain && last_ch != '.' { + Some(result) + } else { + None + } +} diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 04d681b4..f34767c9 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -38,10 +38,12 @@ pub mod auth; pub mod blob; pub mod changes; pub mod email; +pub mod identity; pub mod mailbox; pub mod push; pub mod services; pub mod sieve; +pub mod submission; pub mod thread; pub mod vacation; @@ -116,7 +118,7 @@ pub struct Bincode { pub inner: T, } -pub enum MaybeError { +pub enum IngestError { Temporary, Permanent { code: [u8; 3], reason: String }, } diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs index 315ab6fc..9c521959 100644 --- a/crates/jmap/src/mailbox/get.rs +++ b/crates/jmap/src/mailbox/get.rs @@ -60,7 +60,7 @@ impl JMAP { ) }); let mut response = GetResponse { - account_id: Some(request.account_id), + account_id: request.account_id.into(), state: self .get_state(account_id, Collection::Mailbox) .await? diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 9e038a63..ced93434 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -119,7 +119,7 @@ impl JMAP { // Obtain mailbox let document_id = id.document_id(); - if let Some(mut mailbox) = self + if let Some(mailbox) = self .get_property::>>( account_id, Collection::Mailbox, @@ -152,7 +152,7 @@ impl JMAP { } match self - .mailbox_set_item(object, (document_id, mailbox.take()).into(), &ctx) + .mailbox_set_item(object, (document_id, mailbox).into(), &ctx) .await? { Ok(builder) => { @@ -161,7 +161,6 @@ impl JMAP { .with_account_id(account_id) .with_collection(Collection::Mailbox) .create_document(document_id) - .assert_value(Property::Value, &mailbox) .custom(builder); if !batch.is_empty() { changes.log_update(Collection::Mailbox, document_id); @@ -393,8 +392,7 @@ impl JMAP { .with_account_id(account_id) .with_collection(Collection::Mailbox) .delete_document(document_id) - .assert_value(Property::Value, &mailbox) - .custom(ObjectIndexBuilder::new(SCHEMA).with_current(mailbox.inner)); + .custom(ObjectIndexBuilder::new(SCHEMA).with_current(mailbox)); match self.store.write(batch.build()).await { Ok(_) => { @@ -448,7 +446,7 @@ impl JMAP { async fn mailbox_set_item( &self, changes_: Object, - update: Option<(u32, Object)>, + update: Option<(u32, HashedValue>)>, ctx: &SetContext<'_>, ) -> Result, MethodError> { // Parse properties @@ -496,7 +494,7 @@ impl JMAP { let mut new_value = None; if let Some((_, current_fields)) = update.as_ref() { if let Value::List(subscriptions) = - current_fields.get(&Property::IsSubscribed) + current_fields.inner.get(&Property::IsSubscribed) { if subscribe { if !subscriptions.contains(&account_id) { @@ -647,7 +645,7 @@ impl JMAP { if let Value::Text(mailbox_role) = changes.get(&Property::Role) { if update .as_ref() - .map(|(_, update)| update.get(&Property::Role)) + .map(|(_, update)| update.inner.get(&Property::Role)) .and_then(|v| v.as_string()) .unwrap_or_default() != mailbox_role @@ -694,12 +692,14 @@ impl JMAP { (*mailbox_parent_id).into() } else if let Some((_, current_fields)) = &update { if current_fields + .inner .properties .get(&Property::Name) .and_then(|n| n.as_string()) != Some(mailbox_name) { current_fields + .inner .properties .get(&Property::ParentId) .and_then(|id| id.as_id().map(|id| id.document_id())) diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 73b1be2c..64bf9408 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -2,7 +2,7 @@ use jmap_proto::types::{state::StateChange, type_state::TypeState}; use store::ahash::AHashMap; use utils::ipc::{DeliveryResult, IngestMessage}; -use crate::{mailbox::INBOX_ID, MaybeError, JMAP}; +use crate::{mailbox::INBOX_ID, IngestError, JMAP}; impl JMAP { pub async fn deliver_message(&self, message: IngestMessage) -> Vec { @@ -77,12 +77,12 @@ impl JMAP { } } Err(err) => match err { - MaybeError::Temporary => { + IngestError::Temporary => { *status = DeliveryResult::TemporaryFailure { reason: "Transient server failure.".into(), } } - MaybeError::Permanent { code, reason } => { + IngestError::Permanent { code, reason } => { *status = DeliveryResult::PermanentFailure { code, reason: reason.into(), diff --git a/crates/jmap/src/sieve/get.rs b/crates/jmap/src/sieve/get.rs index c1413199..5812be38 100644 --- a/crates/jmap/src/sieve/get.rs +++ b/crates/jmap/src/sieve/get.rs @@ -36,7 +36,7 @@ impl JMAP { .collect::>() }; let mut response = GetResponse { - account_id: Some(request.account_id), + account_id: request.account_id.into(), state: self .get_state(account_id, Collection::SieveScript) .await? diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 4a058ff6..3ba928f3 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -13,7 +13,7 @@ use crate::{ email::ingest::{IngestEmail, IngestedEmail}, mailbox::{INBOX_ID, TRASH_ID}, sieve::SeenIdHash, - Bincode, MaybeError, JMAP, + Bincode, IngestError, JMAP, }; use super::ActiveScript; @@ -33,12 +33,12 @@ impl JMAP { envelope_to: &str, account_id: u32, mut active_script: ActiveScript, - ) -> Result { + ) -> Result { // Parse message let message = if let Some(message) = Message::parse(raw_message) { message } else { - return Err(MaybeError::Permanent { + return Err(IngestError::Permanent { code: [5, 5, 0], reason: "Failed to parse message.".to_string(), }); @@ -48,7 +48,7 @@ impl JMAP { let mailbox_ids = self .mailbox_get_or_create(account_id) .await - .map_err(|_| MaybeError::Temporary)?; + .map_err(|_| IngestError::Temporary)?; // Create Sieve instance let mut instance = self.sieve_runtime.filter_parsed(message); @@ -445,7 +445,7 @@ impl JMAP { } if let Some(reject_reason) = reject_reason { - Err(MaybeError::Permanent { + Err(IngestError::Permanent { code: [5, 7, 1], reason: reject_reason, }) @@ -453,7 +453,7 @@ impl JMAP { Ok(ingested_message) } else { // There were problems during delivery - Err(MaybeError::Temporary) + Err(IngestError::Temporary) } } } diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 1623e549..fdac1a4b 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -128,7 +128,7 @@ impl JMAP { // Obtain sieve script let document_id = id.document_id(); - if let Some(mut sieve) = self + if let Some(sieve) = self .get_property::>>( account_id, Collection::SieveScript, @@ -138,7 +138,7 @@ impl JMAP { .await? { match self - .sieve_set_item(object, (document_id, sieve.take()).into(), &ctx) + .sieve_set_item(object, (document_id, sieve).into(), &ctx) .await? { Ok((builder, blob)) => { @@ -158,7 +158,6 @@ impl JMAP { .with_account_id(account_id) .with_collection(Collection::SieveScript) .update_document(document_id) - .assert_value(Property::Value, &sieve) .custom(builder); if !batch.is_empty() { changes.log_update(Collection::SieveScript, document_id); @@ -272,7 +271,7 @@ impl JMAP { ) -> Result { // Fetch record let obj = self - .get_property::>( + .get_property::>>( account_id, Collection::SieveScript, document_id, @@ -293,7 +292,7 @@ impl JMAP { // Make sure the script is not active if fail_if_active && matches!( - obj.properties.get(&Property::IsActive), + obj.inner.properties.get(&Property::IsActive), Some(Value::Bool(true)) ) { @@ -323,11 +322,11 @@ impl JMAP { async fn sieve_set_item( &self, changes_: Object, - update: Option<(u32, Object)>, + update: Option<(u32, HashedValue>)>, ctx: &SetContext<'_>, ) -> Result>), SetError>, MethodError> { // Vacation script cannot be modified - if matches!(update.as_ref().and_then(|(_, obj)| obj.properties.get(&Property::Name)), Some(Value::Text ( value )) if value.eq_ignore_ascii_case("vacation")) + if matches!(update.as_ref().and_then(|(_, obj)| obj.inner.properties.get(&Property::Name)), Some(Value::Text ( value )) if value.eq_ignore_ascii_case("vacation")) { return Ok(Err(SetError::forbidden().with_description( "The 'vacation' script cannot be modified, use VacationResponse/set instead.", @@ -358,7 +357,7 @@ impl JMAP { ))); } else if update .as_ref() - .and_then(|(_, obj)| obj.properties.get(&Property::Name)) + .and_then(|(_, obj)| obj.inner.properties.get(&Property::Name)) .map_or( true, |p| matches!(p, Value::Text (prev_value ) if prev_value != &value), @@ -516,13 +515,12 @@ impl JMAP { batch .update_document(document_id) .value(Property::EmailIds, (), F_VALUE | F_CLEAR) - .assert_value(Property::Value, &sieve) .custom( ObjectIndexBuilder::new(SCHEMA) .with_changes( Object::with_capacity(1).with_property(Property::IsActive, false), ) - .with_current(sieve.inner), + .with_current(sieve), ); changed_ids.push((document_id, false)); } @@ -539,16 +537,13 @@ impl JMAP { ) .await? { - batch - .update_document(document_id) - .assert_value(Property::Value, &sieve) - .custom( - ObjectIndexBuilder::new(SCHEMA) - .with_changes( - Object::with_capacity(1).with_property(Property::IsActive, true), - ) - .with_current(sieve.inner), - ); + batch.update_document(document_id).custom( + ObjectIndexBuilder::new(SCHEMA) + .with_changes( + Object::with_capacity(1).with_property(Property::IsActive, true), + ) + .with_current(sieve), + ); changed_ids.push((document_id, true)); } } diff --git a/crates/jmap/src/submission/get.rs b/crates/jmap/src/submission/get.rs new file mode 100644 index 00000000..e9e76943 --- /dev/null +++ b/crates/jmap/src/submission/get.rs @@ -0,0 +1,167 @@ +use jmap_proto::{ + error::method::MethodError, + method::get::{GetRequest, GetResponse, RequestArguments}, + object::Object, + types::{collection::Collection, property::Property, value::Value}, +}; +use smtp::{core::management::QueueRequest, queue}; +use tokio::sync::oneshot; + +use crate::JMAP; + +impl JMAP { + pub async fn email_submission_get( + &self, + mut request: GetRequest, + ) -> Result { + let ids = request.unwrap_ids(self.config.get_max_objects)?; + let properties = request.unwrap_properties(&[ + Property::Id, + Property::EmailId, + Property::IdentityId, + Property::ThreadId, + Property::Envelope, + Property::SendAt, + Property::UndoStatus, + Property::DeliveryStatus, + Property::DsnBlobIds, + Property::MdnBlobIds, + ]); + let account_id = request.account_id.document_id(); + let email_submission_ids = self + .get_document_ids(account_id, Collection::EmailSubmission) + .await? + .unwrap_or_default(); + let ids = if let Some(ids) = ids { + ids + } else { + email_submission_ids + .iter() + .take(self.config.get_max_objects) + .map(Into::into) + .collect::>() + }; + let mut response = GetResponse { + account_id: request.account_id.into(), + state: self + .get_state(account_id, Collection::EmailSubmission) + .await? + .into(), + list: Vec::with_capacity(ids.len()), + not_found: vec![], + }; + + for id in ids { + // Obtain the email_submission object + let document_id = id.document_id(); + if !email_submission_ids.contains(document_id) { + response.not_found.push(id); + continue; + } + let mut push = if let Some(push) = self + .get_property::>( + account_id, + Collection::EmailSubmission, + document_id, + Property::Value, + ) + .await? + { + push + } else { + response.not_found.push(id); + continue; + }; + + // Obtain queueId + let mut queued_message = None; + let (result_tx, result_rx) = oneshot::channel(); + if self + .smtp + .queue + .tx + .send(queue::Event::Manage(QueueRequest::Status { + queue_ids: vec![push.get(&Property::MessageId).as_uint().unwrap_or(u64::MAX)], + result_tx, + })) + .await + .is_ok() + { + queued_message = result_rx + .await + .ok() + .and_then(|mut result| result.pop()) + .flatten(); + } + + let mut result = Object::with_capacity(properties.len()); + for property in &properties { + let value = match property { + Property::Id => Value::Id(id), + Property::DeliveryStatus => { + match (queued_message.as_ref(), push.remove(property)) { + (Some(message), Value::Object(mut status)) => { + for rcpt in message + .domains + .iter() + .flat_map(|rcpts| rcpts.recipients.iter()) + { + status.set( + Property::_T(rcpt.address.clone()), + Object::with_capacity(3) + .with_property( + Property::Delivered, + match &rcpt.status { + queue::Status::Scheduled + | queue::Status::TemporaryFailure(_) => { + "queued" + } + queue::Status::Completed(_) => "yes", + queue::Status::PermanentFailure(_) => "no", + }, + ) + .with_property( + Property::SmtpReply, + match &rcpt.status { + queue::Status::Completed(reply) + | queue::Status::TemporaryFailure(reply) + | queue::Status::PermanentFailure(reply) => { + reply.as_str() + } + queue::Status::Scheduled => "250 2.1.5 Queued", + } + .replace('\n', " "), + ) + .with_property(Property::Displayed, "unknown"), + ); + } + + Value::Object(status) + } + (_, value) => value, + } + } + Property::UndoStatus => { + if queued_message.is_some() { + Value::Text("pending".to_string()) + } else { + push.remove(property) + } + } + Property::EmailId + | Property::IdentityId + | Property::ThreadId + | Property::Envelope + | Property::SendAt => push.remove(property), + Property::MdnBlobIds | Property::DsnBlobIds => Value::List(vec![]), + _ => Value::Null, + }; + + result.append(property.clone(), value); + } + response.list.push(result); + } + + Ok(response) + } +} diff --git a/crates/jmap/src/submission/mod.rs b/crates/jmap/src/submission/mod.rs new file mode 100644 index 00000000..f95943f3 --- /dev/null +++ b/crates/jmap/src/submission/mod.rs @@ -0,0 +1,3 @@ +pub mod get; +pub mod query; +pub mod set; diff --git a/crates/jmap/src/submission/query.rs b/crates/jmap/src/submission/query.rs new file mode 100644 index 00000000..7de59d28 --- /dev/null +++ b/crates/jmap/src/submission/query.rs @@ -0,0 +1,92 @@ +use jmap_proto::{ + error::method::MethodError, + method::query::{ + Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty, + }, + types::{collection::Collection, property::Property}, +}; +use store::query::{self}; + +use crate::JMAP; + +impl JMAP { + pub async fn email_submission_query( + &self, + mut request: QueryRequest, + ) -> Result { + let account_id = request.account_id.document_id(); + let mut filters = Vec::with_capacity(request.filter.len()); + + for cond in std::mem::take(&mut request.filter) { + match cond { + Filter::IdentityIds(ids) => { + filters.push(query::Filter::Or); + for id in ids { + filters.push(query::Filter::eq(Property::IdentityId, id.document_id())); + } + filters.push(query::Filter::End); + } + Filter::EmailIds(ids) => { + filters.push(query::Filter::Or); + for id in ids { + filters.push(query::Filter::eq(Property::EmailId, id.id())); + } + filters.push(query::Filter::End); + } + Filter::ThreadIds(ids) => { + filters.push(query::Filter::Or); + for id in ids { + filters.push(query::Filter::eq(Property::ThreadId, id.document_id())); + } + filters.push(query::Filter::End); + } + Filter::UndoStatus(undo_status) => { + filters.push(query::Filter::eq(Property::UndoStatus, undo_status)) + } + Filter::Before(before) => filters.push(query::Filter::lt( + Property::SendAt, + before.timestamp() as u64, + )), + Filter::After(after) => filters.push(query::Filter::gt( + Property::SendAt, + after.timestamp() as u64, + )), + other => return Err(MethodError::UnsupportedFilter(other.to_string())), + } + } + + let result_set = self + .filter(account_id, Collection::EmailSubmission, filters) + .await?; + + let (response, paginate) = self.build_query_response(&result_set, &request).await?; + + if let Some(paginate) = paginate { + // Parse sort criteria + let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len())); + for comparator in request + .sort + .and_then(|s| if !s.is_empty() { s.into() } else { None }) + .unwrap_or_else(|| vec![Comparator::descending(SortProperty::SentAt)]) + { + comparators.push(match comparator.property { + SortProperty::EmailId => { + query::Comparator::field(Property::EmailId, comparator.is_ascending) + } + SortProperty::ThreadId => { + query::Comparator::field(Property::ThreadId, comparator.is_ascending) + } + SortProperty::SentAt => { + query::Comparator::field(Property::SendAt, comparator.is_ascending) + } + other => return Err(MethodError::UnsupportedSort(other.to_string())), + }); + } + + // Sort results + self.sort(result_set, comparators, paginate, response).await + } else { + Ok(response) + } + } +} diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs new file mode 100644 index 00000000..b7b25984 --- /dev/null +++ b/crates/jmap/src/submission/set.rs @@ -0,0 +1,695 @@ +use std::{collections::HashMap, sync::Arc}; + +use jmap_proto::{ + error::{ + method::MethodError, + set::{SetError, SetErrorType}, + }, + method::set::{self, SetRequest, SetResponse}, + object::{ + email_submission::SetArguments, + index::{IndexAs, IndexProperty, ObjectIndexBuilder}, + Object, + }, + request::{ + method::{MethodFunction, MethodName, MethodObject}, + reference::MaybeReference, + Call, RequestMethod, + }, + response::references::EvalObjectReferences, + types::{ + collection::Collection, + date::UTCDate, + property::Property, + value::{MaybePatchValue, SetValue, Value}, + }, +}; +use smtp::{ + core::{management::QueueRequest, NullIo, Session, SessionData, State}, + queue, +}; +use smtp_proto::{request::parser::Rfc5321Parser, MailFrom, RcptTo}; +use store::{ + write::{assert::HashedValue, log::ChangeLogBuilder, now, BatchBuilder}, + BlobKind, +}; +use tokio::sync::oneshot; +use utils::{listener::ServerInstance, map::vec_map::VecMap}; + +use crate::{identity::set::sanitize_email, JMAP}; + +pub static SCHEMA: &[IndexProperty] = &[ + IndexProperty::new(Property::UndoStatus).index_as(IndexAs::Text { + tokenize: false, + index: true, + }), + IndexProperty::new(Property::EmailId).index_as(IndexAs::LongInteger), + IndexProperty::new(Property::IdentityId).index_as(IndexAs::Integer), + IndexProperty::new(Property::ThreadId).index_as(IndexAs::Integer), + IndexProperty::new(Property::SendAt).index_as(IndexAs::LongInteger), +]; + +impl JMAP { + pub async fn email_submission_set( + &self, + mut request: SetRequest, + instance: &Arc, + next_call: &mut Option>, + ) -> Result { + let account_id = request.account_id.document_id(); + let mut response = SetResponse::from_request(&request, self.config.set_max_objects)?; + let will_destroy = request.unwrap_destroy(); + + // Process creates + let mut changes = ChangeLogBuilder::new(); + let mut success_email_ids = HashMap::new(); + for (id, object) in request.unwrap_create() { + match self + .send_message(account_id, &response, instance, object) + .await? + { + Ok(submission) => { + // Add id mapping + success_email_ids.insert( + id.clone(), + *submission.get(&Property::EmailId).as_id().unwrap(), + ); + + // Insert record + let mut batch = BatchBuilder::new(); + let document_id = self + .assign_document_id(account_id, Collection::EmailSubmission) + .await?; + batch + .with_account_id(account_id) + .with_collection(Collection::EmailSubmission) + .create_document(document_id) + .custom(ObjectIndexBuilder::new(SCHEMA).with_changes(submission)); + self.write_batch(batch).await?; + changes.log_insert(Collection::EmailSubmission, document_id); + response.created(id, document_id); + } + Err(err) => { + response.not_created.append(id, err); + } + } + } + + // Process updates + 'update: for (id, object) in request.unwrap_update() { + // Make sure id won't be destroyed + if will_destroy.contains(&id) { + response.not_updated.append(id, SetError::will_destroy()); + continue 'update; + } + + // Obtain submission + let document_id = id.document_id(); + let submission = if let Some(submission) = self + .get_property::>>( + account_id, + Collection::EmailSubmission, + document_id, + Property::Value, + ) + .await? + { + submission + } else { + response.not_updated.append(id, SetError::not_found()); + continue 'update; + }; + + let mut queue_id = u64::MAX; + let mut undo_status = None; + + for (property, value) in object.properties { + let value = match response.eval_object_references(value) { + Ok(value) => value, + Err(err) => { + response.not_updated.append(id, err); + continue 'update; + } + }; + if let ( + Property::UndoStatus, + MaybePatchValue::Value(Value::Text(undo_status_)), + Value::UnsignedInt(queue_id_), + ) = (&property, value, submission.inner.get(&Property::MessageId)) + { + undo_status = undo_status_.into(); + queue_id = *queue_id_; + } else { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(property) + .with_description("Field could not be set."), + ); + continue 'update; + } + } + + match undo_status { + Some(undo_status) if undo_status == "canceled" => { + let (result_tx, result_rx) = oneshot::channel(); + if self + .smtp + .queue + .tx + .send(queue::Event::Manage(QueueRequest::Cancel { + queue_ids: vec![queue_id], + item: None, + result_tx, + })) + .await + .is_ok() + && result_rx + .await + .ok() + .and_then(|mut r| r.pop()) + .unwrap_or(false) + { + // Update record + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::EmailSubmission) + .update_document(document_id) + .custom( + ObjectIndexBuilder::new(SCHEMA) + .with_current(submission) + .with_changes( + Object::with_capacity(1) + .with_property(Property::UndoStatus, undo_status), + ), + ); + self.write_batch(batch).await?; + changes.log_update(Collection::EmailSubmission, document_id); + response.updated.append(id, None); + } else { + response.not_updated.append( + id, + SetError::new(SetErrorType::CannotUnsend).with_description( + "The requested message is no longer in the queue.", + ), + ); + } + } + Some(_) => { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(Property::UndoStatus) + .with_description("Email submissions can only be cancelled."), + ); + } + None => { + response.not_updated.append( + id, + SetError::invalid_properties() + .with_description("No properties to set were found."), + ); + } + } + } + + // Process deletions + for id in will_destroy { + let document_id = id.document_id(); + if let Some(submission) = self + .get_property::>>( + account_id, + Collection::EmailSubmission, + document_id, + Property::Value, + ) + .await? + { + // Update record + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::EmailSubmission) + .delete_document(document_id) + .custom(ObjectIndexBuilder::new(SCHEMA).with_current(submission)); + self.write_batch(batch).await?; + changes.log_delete(Collection::EmailSubmission, document_id); + response.destroyed.push(id); + } else { + response.not_destroyed.append(id, SetError::not_found()); + } + } + + // Write changes + if !changes.is_empty() { + response.new_state = self.commit_changes(account_id, changes).await?.into(); + } + + // On success + if (request + .arguments + .on_success_destroy_email + .as_ref() + .map_or(false, |p| !p.is_empty()) + || request + .arguments + .on_success_update_email + .as_ref() + .map_or(false, |p| !p.is_empty())) + && response.has_changes() + { + *next_call = Call { + id: String::new(), + name: MethodName::new(MethodObject::Email, MethodFunction::Set), + method: RequestMethod::Set(SetRequest { + account_id: request.account_id, + if_in_state: None, + create: None, + update: request.arguments.on_success_update_email.map(|update| { + update + .into_iter() + .filter_map(|(id, value)| { + ( + match id { + MaybeReference::Value(id) => id, + MaybeReference::Reference(id_ref) => { + *(success_email_ids.get(&id_ref)?) + } + }, + value, + ) + .into() + }) + .collect() + }), + destroy: request.arguments.on_success_destroy_email.map(|ids| { + MaybeReference::Value( + ids.into_iter() + .filter_map(|id| match id { + MaybeReference::Value(id) => Some(id), + MaybeReference::Reference(id_ref) => { + success_email_ids.get(&id_ref).copied() + } + }) + .collect(), + ) + }), + arguments: set::RequestArguments::Email, + }), + } + .into(); + } + + Ok(response) + } + + async fn send_message( + &self, + account_id: u32, + response: &SetResponse, + instance: &Arc, + object: Object, + ) -> Result, SetError>, MethodError> { + let mut submission = Object::with_capacity(object.properties.len()); + let mut email_id = u32::MAX; + let mut identity_id = u32::MAX; + let mut mail_from = None; + let mut rcpt_to: Vec> = Vec::new(); + + for (property, value) in object.properties { + let value = match response.eval_object_references(value) { + Ok(value) => value, + Err(err) => { + return Ok(Err(err)); + } + }; + + let value = match (&property, value) { + (Property::EmailId, MaybePatchValue::Value(Value::Id(value))) => { + submission.append(Property::ThreadId, Value::Id(value.prefix_id().into())); + email_id = value.document_id(); + Value::Id(value) + } + (Property::IdentityId, MaybePatchValue::Value(Value::Id(value))) => { + identity_id = value.document_id(); + Value::Id(value) + } + (Property::Envelope, MaybePatchValue::Value(Value::Object(value))) => { + for (property, value) in &value.properties { + match (property, value) { + (Property::MailFrom, _) => match parse_envelope_address(value) { + Ok((addr, params)) => { + match Rfc5321Parser::new( + &mut params + .as_ref() + .map_or(&b"\n"[..], |p| p.as_bytes()) + .iter(), + ) + .mail_from_parameters(addr) + { + Ok(addr) => { + mail_from = addr.into(); + } + Err(err) => { + return Ok(Err(SetError::invalid_properties() + .with_property(Property::Envelope) + .with_description(format!( + "Failed to parse mailFrom parameters: {err}." + )))); + } + } + } + Err(err) => { + return Ok(Err(err)); + } + }, + (Property::RcptTo, Value::List(value)) => { + for addr in value { + match parse_envelope_address(addr) { + Ok((addr, params)) => { + match Rfc5321Parser::new( + &mut params + .as_ref() + .map_or(&b"\n"[..], |p| p.as_bytes()) + .iter(), + ) + .rcpt_to_parameters(addr) + { + Ok(addr) => { + if !rcpt_to + .iter() + .any(|rcpt| rcpt.address == addr.address) + { + rcpt_to.push(addr); + } + } + Err(err) => { + return Ok(Err(SetError::invalid_properties() + .with_property(Property::Envelope) + .with_description(format!( + "Failed to parse rcptTo parameters: {err}." + )))); + } + } + } + Err(err) => { + return Ok(Err(err)); + } + } + } + } + _ => { + return Ok(Err(SetError::invalid_properties() + .with_property(Property::Envelope) + .with_description(format!( + "Invalid object property {property}." + )))); + } + } + } + Value::Object(value) + } + (Property::Envelope, MaybePatchValue::Value(Value::Null)) => { + continue; + } + (Property::UndoStatus, MaybePatchValue::Value(Value::Text(_))) => continue, + _ => { + return Ok(Err(SetError::invalid_properties() + .with_property(property) + .with_description("Field could not be set."))); + } + }; + + submission.append(property, value); + } + + // Make sure we have all required fields. + if email_id == u32::MAX || identity_id == u32::MAX { + return Ok(Err(SetError::invalid_properties() + .with_properties([Property::EmailId, Property::IdentityId]) + .with_description( + "emailId and identityId properties are required.", + ))); + } + + // Fetch identity's mailFrom + let identity_mail_from = if let Some(identity_mail_from) = self + .get_property::>( + account_id, + Collection::Identity, + identity_id, + Property::Value, + ) + .await? + .and_then(|mut obj| obj.properties.remove(&Property::Email)) + .and_then(|value| value.try_unwrap_string()) + { + identity_mail_from + } else { + return Ok(Err(SetError::invalid_properties() + .with_property(Property::IdentityId) + .with_description("Identity not found."))); + }; + + // Make sure the envelope address matches the identity email address + let mail_from = if let Some(mail_from) = mail_from { + if !mail_from.address.eq_ignore_ascii_case(&identity_mail_from) { + return Ok(Err(SetError::new(SetErrorType::ForbiddenFrom) + .with_description( + "Envelope mailFrom does not match identity email address.", + ))); + } + mail_from + } else { + submission + .properties + .get_mut_or_insert_with(Property::Envelope, || { + Value::Object(Object::with_capacity(2)) + }) + .as_obj_mut() + .unwrap() + .set( + Property::MailFrom, + Object::with_capacity(1) + .with_property(Property::Email, identity_mail_from.clone()), + ); + MailFrom { + address: identity_mail_from, + ..Default::default() + } + }; + + // Add recipients to envelope if missing + if rcpt_to.is_empty() { + if let Some(obj) = self + .get_property::>( + account_id, + Collection::Email, + email_id, + Property::BodyStructure, + ) + .await? + { + let mut envelope_values = Vec::new(); + for property in &[Property::To, Property::Cc, Property::Bcc] { + if let Some(Value::List(addresses)) = obj.properties.get(property) { + for address in addresses { + if let Some(address) = address + .as_obj() + .and_then(|obj| obj.properties.get(&Property::Email)) + .and_then(|value| value.as_string()) + .and_then(sanitize_email) + { + if !rcpt_to.iter().any(|rcpt| rcpt.address == address) { + envelope_values.push(Value::Object( + Object::with_capacity(1) + .with_property(Property::Email, address.clone()), + )); + rcpt_to.push(RcptTo { + address, + ..Default::default() + }); + } + } + } + } + } + if !rcpt_to.is_empty() { + submission + .properties + .get_mut_or_insert_with(Property::Envelope, || { + Value::Object(Object::with_capacity(1)) + }) + .as_obj_mut() + .unwrap() + .set(Property::RcptTo, Value::List(envelope_values)); + } else { + return Ok(Err(SetError::new(SetErrorType::NoRecipients) + .with_description("No recipients found in email."))); + } + } else { + return Ok(Err(SetError::invalid_properties() + .with_property(Property::EmailId) + .with_description("Email not found."))); + } + } + + // Update sendAt + submission.append( + Property::SendAt, + UTCDate::from_timestamp(if mail_from.hold_until > 0 { + mail_from.hold_until + } else if mail_from.hold_for > 0 { + mail_from.hold_for + now() + } else { + now() + } as i64), + ); + + // Obtain raw message + let message = if let Some(message) = self + .get_blob( + &BlobKind::LinkedMaildir { + account_id, + document_id: email_id, + }, + 0..u32::MAX, + ) + .await? + { + if message.len() > self.config.mail_max_size { + return Ok(Err(SetError::new(SetErrorType::InvalidEmail) + .with_description(format!( + "Message exceeds maximum size of {} bytes.", + self.config.mail_max_size + )))); + } + + message + } else { + return Ok(Err(SetError::invalid_properties() + .with_property(Property::EmailId) + .with_description("Blob for email not found."))); + }; + + // Begin local SMTP session + let mut session = + Session::::local(self.smtp.clone(), instance.clone(), SessionData::default()); + + // MAIL FROM + let _ = session.handle_mail_from(mail_from).await; + if let Some(error) = session.has_failed() { + return Ok(Err(SetError::new(SetErrorType::ForbiddenMailFrom) + .with_description(format!( + "Server rejected MAIL-FROM: {}", + error.trim() + )))); + } + + // RCPT TO + let mut responses = Vec::new(); + let mut has_success = false; + for rcpt in rcpt_to { + let addr = rcpt.address.clone(); + let _ = session.handle_rcpt_to(rcpt).await; + let response = session.has_failed(); + if response.is_none() { + has_success = true; + } + responses.push((addr, response)); + } + + // DATA + if has_success { + session.data.message = message; + let response = session.queue_message().await; + if let State::Accepted(queue_id) = session.state { + submission.append(Property::MessageId, queue_id); + } else { + return Ok(Err(SetError::new(SetErrorType::ForbiddenToSend) + .with_description(format!( + "Server rejected DATA: {}", + std::str::from_utf8(&response).unwrap().trim() + )))); + } + } + + // Set responses + submission.append( + Property::UndoStatus, + if has_success { "final" } else { "failed" }, + ); + submission.append( + Property::DeliveryStatus, + Object { + properties: responses + .into_iter() + .map(|(addr, response)| { + ( + Property::_T(addr), + Value::Object( + Object::with_capacity(3) + .with_property( + Property::Delivered, + if response.is_none() { "unknown" } else { "no" }, + ) + .with_property( + Property::SmtpReply, + response.unwrap_or_else(|| "250 2.1.5 Queued".to_string()), + ) + .with_property(Property::Displayed, "unknown"), + ), + ) + }) + .collect::>(), + }, + ); + + Ok(Ok(submission)) + } +} + +fn parse_envelope_address(envelope: &Value) -> Result<(String, Option), SetError> { + if let Value::Object(envelope) = envelope { + if let Some(Value::Text(addr)) = envelope.properties.get(&Property::Email) { + if let Some(addr) = sanitize_email(addr) { + if let Some(Value::Object(params)) = envelope.properties.get(&Property::Parameters) + { + let mut params_text = String::new(); + for (k, v) in params.properties.iter() { + if let Property::_T(k) = &k { + if !k.is_empty() { + if !params_text.is_empty() { + params_text.push(' '); + } + params_text.push_str(k); + if let Value::Text(v) = v { + params_text.push('='); + params_text.push_str(v); + } + } + } + } + params_text.push('\n'); + + Ok((addr, Some(params_text))) + } else { + Ok((addr, None)) + } + } else { + Err(SetError::invalid_properties() + .with_property(Property::Envelope) + .with_description(format!("Invalid e-mail address {addr:?}."))) + } + } else { + Err(SetError::invalid_properties() + .with_property(Property::Envelope) + .with_description("Missing e-mail address field.")) + } + } else { + Err(SetError::invalid_properties() + .with_property(Property::Envelope) + .with_description("Invalid envelope object.")) + } +} diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs index 7445f9d5..a5bde04a 100644 --- a/crates/jmap/src/thread/get.rs +++ b/crates/jmap/src/thread/get.rs @@ -29,7 +29,7 @@ impl JMAP { .properties .map_or(true, |p| p.unwrap().contains(&Property::EmailIds)); let mut response = GetResponse { - account_id: Some(request.account_id), + account_id: request.account_id.into(), state: self.get_state(account_id, Collection::Thread).await?.into(), list: Vec::with_capacity(ids.len()), not_found: vec![], diff --git a/crates/jmap/src/vacation/get.rs b/crates/jmap/src/vacation/get.rs index f76a31e8..79efd75f 100644 --- a/crates/jmap/src/vacation/get.rs +++ b/crates/jmap/src/vacation/get.rs @@ -25,7 +25,7 @@ impl JMAP { Property::HtmlBody, ]); let mut response = GetResponse { - account_id: Some(request.account_id), + account_id: request.account_id.into(), state: self .get_state(account_id, Collection::SieveScript) .await? diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index e4a58a96..27a00de1 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -189,12 +189,9 @@ impl JMAP { ) .await? .map(|value| { - batch - .update_document(document_id) - .assert_value(Property::Value, &value); was_active = value.inner.properties.get(&Property::IsActive) == Some(&Value::Bool(true)); - value.inner + value }) .ok_or(MethodError::ServerPartialFail)? .into() @@ -213,6 +210,7 @@ impl JMAP { // Write changes let document_id = if let Some(document_id) = document_id { batch + .update_document(document_id) .value(Property::EmailIds, (), F_VALUE | F_CLEAR) .custom(obj); change_log.log_insert(Collection::SieveScript, document_id); diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 4650a991..3371eff9 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -58,7 +58,7 @@ use crate::{ dane::{DnssecResolver, Tlsa}, mta_sts, }, - queue::{self, DomainPart, QuotaLimiter}, + queue::{self, DomainPart, QueueId, QuotaLimiter}, reporting, }; @@ -162,6 +162,7 @@ pub enum State { Sasl(LineReceiver), DataTooLarge(DummyDataReceiver), RequestTooLarge(DummyLineReceiver), + Accepted(QueueId), None, } @@ -355,30 +356,34 @@ impl PartialOrd for SessionAddress { } #[cfg(feature = "local_delivery")] -pub struct NullIo(); +#[derive(Default)] +pub struct NullIo { + pub tx_buf: Vec, +} #[cfg(feature = "local_delivery")] impl AsyncWrite for NullIo { fn poll_write( - self: std::pin::Pin<&mut Self>, + mut self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, - _buf: &[u8], + buf: &[u8], ) -> std::task::Poll> { - unreachable!() + self.tx_buf.extend_from_slice(buf); + std::task::Poll::Ready(Ok(buf.len())) } fn poll_flush( self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - unreachable!() + std::task::Poll::Ready(Ok(())) } fn poll_shutdown( self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - unreachable!() + std::task::Poll::Ready(Ok(())) } } @@ -422,9 +427,7 @@ impl Session { pub fn local( core: std::sync::Arc, instance: std::sync::Arc, - mail_from: SessionAddress, - rcpt_to: Vec, - message: Vec, + data: SessionData, ) -> Self { Session { state: State::None, @@ -432,36 +435,21 @@ impl Session { core, span: tracing::info_span!( "local_delivery", - "return_path" = if !mail_from.address_lcase.is_empty() { - mail_from.address_lcase.as_str() - } else { - "<>" - }, - "nrcpt" = rcpt_to.len(), - "size" = message.len(), + "return_path" = + if let Some(addr) = data.mail_from.as_ref().map(|a| a.address_lcase.as_str()) { + if !addr.is_empty() { + addr + } else { + "<>" + } + } else { + "<>" + }, + "nrcpt" = data.rcpt_to.len(), + "size" = data.message.len(), ), - stream: NullIo(), - data: SessionData { - local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), - remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), - helo_domain: "localhost".into(), - mail_from: mail_from.into(), - rcpt_to, - rcpt_errors: 0, - message, - authenticated_as: "".into(), - auth_errors: 0, - priority: 0, - delivery_by: 0, - future_release: 0, - valid_until: Instant::now(), - bytes_left: 0, - messages_sent: 0, - iprev: None, - spf_ehlo: None, - spf_mail_from: None, - dnsbl_error: None, - }, + stream: NullIo::default(), + data, params: SessionParameters { timeout: Default::default(), ehlo_require: Default::default(), @@ -496,7 +484,63 @@ impl Session { rcpt_to: Vec, message: Vec, ) -> Self { - Self::local(core, SIEVE.clone(), mail_from, rcpt_to, message) + Self::local( + core, + SIEVE.clone(), + SessionData::local(mail_from.into(), rcpt_to, message), + ) + } + + pub fn has_failed(&mut self) -> Option { + if self.stream.tx_buf.first().map_or(true, |&c| c == b'2') { + self.stream.tx_buf.clear(); + None + } else { + let response = std::str::from_utf8(&self.stream.tx_buf) + .unwrap() + .trim() + .to_string(); + self.stream.tx_buf.clear(); + Some(response) + } + } +} + +#[cfg(feature = "local_delivery")] +impl SessionData { + pub fn local( + mail_from: Option, + rcpt_to: Vec, + message: Vec, + ) -> Self { + SessionData { + local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + helo_domain: "localhost".into(), + mail_from, + rcpt_to, + rcpt_errors: 0, + message, + authenticated_as: "local".into(), + auth_errors: 0, + priority: 0, + delivery_by: 0, + future_release: 0, + valid_until: Instant::now(), + bytes_left: 0, + messages_sent: 0, + iprev: None, + spf_ehlo: None, + spf_mail_from: None, + dnsbl_error: None, + } + } +} + +#[cfg(feature = "local_delivery")] +impl Default for SessionData { + fn default() -> Self { + Self::local(None, vec![], vec![]) } } diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 35f243df..93f569dd 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -44,7 +44,7 @@ use tokio::{ use crate::{ config::DNSBL_FROM, - core::{scripts::ScriptResult, Session, SessionAddress}, + core::{scripts::ScriptResult, Session, SessionAddress, State}, queue::{self, DomainPart, Message, SimpleEnvelope}, reporting::analysis::AnalyzeReport, }; @@ -489,12 +489,14 @@ impl Session { // Verify queue quota if self.core.queue.has_quota(&mut message).await { + let queue_id = message.id; if self .core .queue .queue_message(message, Some(&headers), &raw_message, &self.span) .await { + self.state = State::Accepted(queue_id); self.data.messages_sent += 1; (b"250 2.0.0 Message queued for delivery.\r\n"[..]).into() } else { diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index c14fea18..5c08026e 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -315,7 +315,7 @@ impl Session { break 'outer; } } - State::None => unreachable!(), + State::None | State::Accepted(_) => unreachable!(), } } self.state = state; diff --git a/crates/store/src/write/assert.rs b/crates/store/src/write/assert.rs index 28b06e0b..3c792786 100644 --- a/crates/store/src/write/assert.rs +++ b/crates/store/src/write/assert.rs @@ -1,6 +1,6 @@ use crate::Deserialize; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct HashedValue { pub hash: u64, pub inner: T, diff --git a/crates/utils/src/listener/limiter.rs b/crates/utils/src/listener/limiter.rs index e144e68e..2d597204 100644 --- a/crates/utils/src/listener/limiter.rs +++ b/crates/utils/src/listener/limiter.rs @@ -10,7 +10,8 @@ use std::{ pub struct RateLimiter { pub max_requests: f64, pub max_interval: f64, - limiter: (Instant, f64), + last_refill: Instant, + tokens: f64, } #[derive(Debug, Clone)] @@ -35,20 +36,21 @@ impl RateLimiter { RateLimiter { max_requests: max_requests as f64, max_interval: max_interval as f64, - limiter: (Instant::now(), max_requests as f64), + last_refill: Instant::now(), + tokens: max_requests as f64, } } pub fn is_allowed(&mut self) -> bool { // Check rate limit - let elapsed = self.limiter.0.elapsed().as_secs_f64(); - self.limiter.1 += elapsed * (self.max_requests / self.max_interval); - if self.limiter.1 > self.max_requests { - self.limiter.1 = self.max_requests; + let elapsed = self.last_refill.elapsed().as_secs_f64(); + self.last_refill = Instant::now(); + self.tokens += elapsed * (self.max_requests / self.max_interval); + if self.tokens > self.max_requests { + self.tokens = self.max_requests; } - if self.limiter.1 >= 1.0 { - self.limiter.0 = Instant::now(); - self.limiter.1 -= 1.0; + if self.tokens >= 1.0 { + self.tokens -= 1.0; true } else { false @@ -58,16 +60,17 @@ impl RateLimiter { pub fn retry_at(&self) -> Instant { Instant::now() + Duration::from_secs( - (self.max_interval as u64).saturating_sub(self.limiter.0.elapsed().as_secs()), + (self.max_interval as u64).saturating_sub(self.last_refill.elapsed().as_secs()), ) } pub fn elapsed(&self) -> Duration { - self.limiter.0.elapsed() + self.last_refill.elapsed() } pub fn reset(&mut self) { - self.limiter = (Instant::now(), self.max_requests); + self.last_refill = Instant::now(); + self.tokens = self.max_requests; } } diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 05782a66..37cf18aa 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -3,10 +3,11 @@ use std::{sync::Arc, time::Duration}; use jmap::JMAP; use jmap_client::{ client::{Client, Credentials}, + core::set::{SetError, SetErrorType}, mailbox::{self}, }; -use crate::jmap::{mailbox::destroy_all_mailboxes, test_account_create}; +use crate::jmap::{mailbox::destroy_all_mailboxes, test_account_create, test_alias_create}; pub async fn test(server: Arc, admin_client: &mut Client) { println!("Running Authorization tests..."); @@ -15,18 +16,19 @@ pub async fn test(server: Arc, admin_client: &mut Client) { let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") .await .to_string(); + test_alias_create(&server, "jdoe@example.com", "john.doe@example.com", false).await; // Wait for rate limit to be restored after running previous tests - tokio::time::sleep(Duration::from_secs(1)).await; + //tokio::time::sleep(Duration::from_secs(1)).await; // Incorrect passwords should be rejected with a 401 error assert!(matches!( - Client::new() - .credentials(Credentials::basic("jdoe@example.com", "abcde")) - .accept_invalid_certs(true) - .connect("https://127.0.0.1:8899") - .await, - Err(jmap_client::Error::Problem(err)) if err.status() == Some(401))); + Client::new() + .credentials(Credentials::basic("jdoe@example.com", "abcde")) + .accept_invalid_certs(true) + .connect("https://127.0.0.1:8899") + .await, + Err(jmap_client::Error::Problem(err)) if err.status() == Some(401))); // Requests should be rate limited let mut n_401 = 0; @@ -44,12 +46,12 @@ pub async fn test(server: Arc, admin_client: &mut Client) { if problem.status().unwrap() == 401 { n_401 += 1; if n_401 > 100 { - panic!("Rate limiter failed."); + panic!("Rate limiter failed: 429: {n_429}, 401: {n_401}."); } } else if problem.status().unwrap() == 429 { n_429 += 1; if n_429 > 11 { - panic!("Rate limiter too restrictive."); + panic!("Rate limiter too restrictive: 429: {n_429}, 401: {n_401}."); } } else { panic!("Unexpected error status {}", problem.status().unwrap()); @@ -92,13 +94,12 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Users should be allowed to create identities only // using email addresses associated to their principal - let implement = "true"; - /*client + let iid1 = client .identity_create("John Doe", "jdoe@example.com") .await .unwrap() .take_id(); - client + let iid2 = client .identity_create("John Doe (secondary)", "john.doe@example.com") .await .unwrap() @@ -111,7 +112,9 @@ pub async fn test(server: Arc, admin_client: &mut Client) { type_: SetErrorType::InvalidProperties, .. })) - ));*/ + )); + client.identity_destroy(&iid1).await.unwrap(); + client.identity_destroy(&iid2).await.unwrap(); // Concurrent requests check let client = Arc::new(client); diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index 359d4bc7..64067a66 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -1,11 +1,14 @@ -use std::{sync::Arc, time::Duration}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; use ahash::AHashMap; -use jmap::{JMAP, SUPERUSER_ID}; +use jmap::JMAP; use jmap_client::{ client::Client, core::set::{SetError, SetErrorType, SetObject}, - email_submission::{Address, Delivered, DeliveryStatus, Displayed, UndoStatus}, + email_submission::{query::Filter, Address, Delivered, DeliveryStatus, Displayed, UndoStatus}, mailbox::Role, Error, }; @@ -18,7 +21,9 @@ use tokio::{ sync::mpsc, }; -use crate::jmap::email_set::assert_email_properties; +use crate::jmap::{ + email_set::assert_email_properties, mailbox::destroy_all_mailboxes, test_account_create, +}; #[derive(Default, Debug, PartialEq, Eq)] pub struct MockMessage { @@ -49,32 +54,26 @@ pub struct MockSMTPSettings { pub do_stop: bool, } -const TEST_DKIM_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY----- -MIICXwIBAAKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFC -jxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gb -to/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB -AoGBALmn+XwWk7akvkUlqb+dOxyLB9i5VBVfje89Teolwc9YJT36BGN/l4e0l6QX -/1//6DWUTB3KI6wFcm7TWJcxbS0tcKZX7FsJvUz1SbQnkS54DJck1EZO/BLa5ckJ -gAYIaqlA9C0ZwM6i58lLlPadX/rtHb7pWzeNcZHjKrjM461ZAkEA+itss2nRlmyO -n1/5yDyCluST4dQfO8kAB3toSEVc7DeFeDhnC1mZdjASZNvdHS4gbLIA1hUGEF9m -3hKsGUMMPwJBAPW5v/U+AWTADFCS22t72NUurgzeAbzb1HWMqO4y4+9Hpjk5wvL/ -eVYizyuce3/fGke7aRYw/ADKygMJdW8H/OcCQQDz5OQb4j2QDpPZc0Nc4QlbvMsj -7p7otWRO5xRa6SzXqqV3+F0VpqvDmshEBkoCydaYwc2o6WQ5EBmExeV8124XAkEA -qZzGsIxVP+sEVRWZmW6KNFSdVUpk3qzK0Tz/WjQMe5z0UunY9Ax9/4PVhp/j61bf -eAYXunajbBSOLlx4D+TunwJBANkPI5S9iylsbLs6NkaMHV6k5ioHBBmgCak95JGX -GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc= ------END RSA PRIVATE KEY-----"#; - #[allow(clippy::disallowed_types)] pub async fn test(server: Arc, client: &mut Client) { println!("Running E-mail submissions tests..."); // Start mock SMTP server let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server(); + server.smtp.resolvers.dns.ipv4_add( + "localhost", + vec!["127.0.0.1".parse().unwrap()], + Instant::now() + std::time::Duration::from_secs(10), + ); + + // Create a test account + let account_id = test_account_create(&server, "jdoe@example.com", "12345", "John Doe") + .await + .to_string(); // Create an identity without using a valid address should fail match client - .set_default_account_id(Id::new(1).to_string()) - .identity_create("John Doe", "jdoe@example.com") + .set_default_account_id(&account_id) + .identity_create("John Doe", "someaddress@domain.com") .await .unwrap_err() { @@ -82,20 +81,8 @@ pub async fn test(server: Arc, client: &mut Client) { err => panic!("Unexpected error: {:?}", err), } - // Create a domain and a test account - let domain_id = client - .set_default_account_id(Id::new(0)) - .domain_create("example.com") - .await - .unwrap() - .take_id(); - let account_id = client - .individual_create("jdoe@example.com", "12345", "John Doe") - .await - .unwrap() - .take_id(); + // Create an identity let identity_id = client - .set_default_account_id(&account_id) .identity_create("John Doe", "jdoe@example.com") .await .unwrap() @@ -151,7 +138,7 @@ pub async fn test(server: Arc, client: &mut Client) { .email_submission_create(&email_id, &identity_id) .await, Err(Error::Set(SetError { - type_: SetErrorType::InvalidProperties, + type_: SetErrorType::NoRecipients, .. })) )); @@ -168,14 +155,14 @@ pub async fn test(server: Arc, client: &mut Client) { ) .await, Err(Error::Set(SetError { - type_: SetErrorType::InvalidProperties, + type_: SetErrorType::ForbiddenFrom, .. })) )); // Submit a valid message submission let email_body = - "From: jdoe@example.com\r\nTo: jane_smith@example.com\r\nSubject: hey\r\n\r\ntest"; + "From: jdoe@example.com\r\nTo: jane_smith@remote.org\r\nSubject: hey\r\n\r\ntest"; let email_id = client .email_import( email_body.as_bytes().to_vec(), @@ -196,10 +183,9 @@ pub async fn test(server: Arc, client: &mut Client) { &mut smtp_rx, MockMessage::new( "", - [""], + [""], email_body, ), - false, ) .await; @@ -221,20 +207,25 @@ pub async fn test(server: Arc, client: &mut Client) { .unwrap() .take_id(); - assert_message_delivery( - &mut smtp_rx, - MockMessage::new( - "", - [ - "", - "", - "", - ], - email_body, - ), - false, - ) - .await; + for _ in 0..3 { + let mut message = expect_message_delivery(&mut smtp_rx).await; + + assert_eq!(message.mail_from, ""); + let rcpt_to = message.rcpt_to.pop().unwrap(); + assert!([ + "", + "", + "", + ] + .contains(&rcpt_to.as_str())); + + assert!( + message.message.contains(email_body), + "Got [{}], Expected[{}]", + message.message, + email_body + ); + } // Confirm that the email submission status was updated tokio::time::sleep(Duration::from_millis(100)).await; @@ -249,27 +240,31 @@ pub async fn test(server: Arc, client: &mut Client) { &AHashMap::from_iter([ ( "tim@foobar.com".to_string(), - DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown) + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown) ), ( "secret_rcpt@test.com".to_string(), - DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown) + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown) ), ( "james@other_domain.com".to_string(), - DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown) + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown) ), ]) ); // SMTP rejects some of the recipients - smtp_settings.lock().fail_rcpt_to = true; let email_submission_id = client .email_submission_create_envelope( &email_id, &identity_id, "jdoe@example.com", - ["tim@foobar.com", "james@other_domain.com", "jane@test.com"], + [ + "nonexistant@example.com", + "delay@other_domain.com", + "fail@test.com", + "tim@foobar.com", + ], ) .await .unwrap() @@ -277,61 +272,64 @@ pub async fn test(server: Arc, client: &mut Client) { assert_message_delivery( &mut smtp_rx, MockMessage::new("", [""], email_body), - false, ) .await; + expect_nothing(&mut smtp_rx).await; - // Confirm that all delivery failures were included + // Verify SMTP replies tokio::time::sleep(Duration::from_millis(100)).await; let email_submission = client .email_submission_get(&email_submission_id, None) .await .unwrap() .unwrap(); - assert_eq!(email_submission.undo_status().unwrap(), &UndoStatus::Final); + assert_eq!( + email_submission.undo_status().unwrap(), + &UndoStatus::Pending + ); assert_eq!( email_submission.delivery_status().unwrap(), &AHashMap::from_iter([ ( - "james@other_domain.com".to_string(), + "nonexistant@example.com".to_string(), DeliveryStatus::new( - "550 I refuse to accept that recipient.", + "550 5.1.2 Mailbox does not exist.", Delivered::No, Displayed::Unknown ) ), ( - "jane@test.com".to_string(), + "delay@other_domain.com".to_string(), DeliveryStatus::new( - "550 I refuse to accept that recipient.", + "Code: 451, Enhanced code: 4.5.3, Message: Try again later.", + Delivered::Queued, + Displayed::Unknown + ) + ), + ( + "fail@test.com".to_string(), + DeliveryStatus::new( + "Code: 550, Enhanced code: 0.0.0, Message: I refuse to accept that recipient.", Delivered::No, Displayed::Unknown ) ), ( "tim@foobar.com".to_string(), - DeliveryStatus::new("250 OK", Delivered::Queued, Displayed::Unknown) + DeliveryStatus::new( + "Code: 250, Enhanced code: 0.0.0, Message: OK", + Delivered::Yes, + Displayed::Unknown + ) ), ]) ); - smtp_settings.lock().fail_rcpt_to = false; - // SMTP rejects the message - smtp_settings.lock().fail_message = true; - let email_submission_id = client - .email_submission_create_envelope( - &email_id, - &identity_id, - "jdoe@example.com", - ["tim@foobar.com", "james@other_domain.com", "jane@test.com"], - ) + // Cancel submission + client + .email_submission_change_status(&email_submission_id, UndoStatus::Canceled) .await - .unwrap() - .take_id(); - expect_nothing(&mut smtp_rx).await; - - // Confirm that all delivery failures were included - tokio::time::sleep(Duration::from_millis(100)).await; + .unwrap(); let email_submission = client .email_submission_get(&email_submission_id, None) .await @@ -345,73 +343,59 @@ pub async fn test(server: Arc, client: &mut Client) { email_submission.delivery_status().unwrap(), &AHashMap::from_iter([ ( - "james@other_domain.com".to_string(), + "nonexistant@example.com".to_string(), DeliveryStatus::new( - "503 Thank you but I am saving myself for dessert.", + "550 5.1.2 Mailbox does not exist.", Delivered::No, Displayed::Unknown ) ), ( - "jane@test.com".to_string(), - DeliveryStatus::new( - "503 Thank you but I am saving myself for dessert.", - Delivered::No, - Displayed::Unknown - ) + "delay@other_domain.com".to_string(), + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown) + ), + ( + "fail@test.com".to_string(), + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown) ), ( "tim@foobar.com".to_string(), - DeliveryStatus::new( - "503 Thank you but I am saving myself for dessert.", - Delivered::No, - Displayed::Unknown - ) + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Unknown, Displayed::Unknown) ), ]) ); - smtp_settings.lock().fail_message = false; - - // Enable DKIM for the domain - client - .set_default_account_id(Id::from(SUPERUSER_ID)) - .domain_enable_dkim(&domain_id, TEST_DKIM_KEY, "my-selector", None) - .await - .unwrap(); - client.set_default_account_id(&account_id); // Confirm that the sendAt property is updated when using FUTURERELEASE + let hold_until = DateTime::parse_rfc3339("2079-11-20T05:00:00Z") + .unwrap() + .to_timestamp(); let email_submission_id = client .email_submission_create_envelope( &email_id, &identity_id, - Address::new("jdoe@example.com").parameter("HOLDUNTIL", Some("2079-11-20T05:00:00Z")), - ["jane_smith@example.com"], + Address::new("jdoe@example.com").parameter("HOLDUNTIL", Some(hold_until.to_string())), + ["jane_smith@remote.org"], ) .await .unwrap() .take_id(); - assert_message_delivery( - &mut smtp_rx, - MockMessage::new( - " HOLDUNTIL=2079-11-20T05:00:00Z", - [""], - email_body, - ), - true, - ) - .await; tokio::time::sleep(Duration::from_millis(100)).await; let email_submission = client .email_submission_get(&email_submission_id, None) .await .unwrap() .unwrap(); + assert_eq!(email_submission.send_at().unwrap(), hold_until); assert_eq!( - email_submission.send_at().unwrap(), - DateTime::parse_rfc3339("2079-11-20T05:00:00Z") - .unwrap() - .to_timestamp() + email_submission.undo_status().unwrap(), + &UndoStatus::Pending + ); + assert_eq!( + email_submission.delivery_status().unwrap(), + &AHashMap::from_iter([( + "jane_smith@remote.org".to_string(), + DeliveryStatus::new("250 2.1.5 Queued", Delivered::Queued, Displayed::Unknown) + ),]) ); // Verify onSuccessUpdateEmail action @@ -434,7 +418,6 @@ pub async fn test(server: Arc, client: &mut Client) { assert_email_properties(client, &email_id, &[&mailbox_id_2], &["$draft"]).await; // Verify onSuccessDestroyEmail action - smtp_settings.lock().do_stop = true; let mut request = client.build(); let set_request = request.set_email_submission(); let create_id = set_request @@ -451,17 +434,20 @@ pub async fn test(server: Arc, client: &mut Client) { .await .unwrap() .is_none()); + smtp_settings.lock().do_stop = true; // Destroy the created mailbox, identity and all submissions - let todo = "true"; - /*client - .set_default_account_id(Id::from(SUPERUSER_ID)) - .principal_destroy(&account_id) + client.identity_destroy(&identity_id).await.unwrap(); + for id in client + .email_submission_query(None::, None::>) .await - .unwrap(); - client.principal_destroy(&domain_id).await.unwrap(); - server.store.principal_purge().unwrap(); - server.store.assert_is_empty();*/ + .unwrap() + .take_ids() + { + client.email_submission_destroy(&id).await.unwrap(); + } + destroy_all_mailboxes(client).await; + server.store.assert_is_empty().await; } pub fn spawn_mock_smtp_server() -> (mpsc::Receiver, Arc>) { @@ -504,12 +490,16 @@ pub fn spawn_mock_smtp_server() -> (mpsc::Receiver, Arc (mpsc::Receiver, Arc, - expected_message: MockMessage, - expect_dkim: bool, -) { +pub async fn expect_message_delivery(event_rx: &mut mpsc::Receiver) -> MockMessage { match tokio::time::timeout(Duration::from_millis(3000), event_rx.recv()).await { Ok(Some(message)) => { println!("Got message [{}]", message.message); - assert_eq!(message.mail_from, expected_message.mail_from); - assert_eq!(message.rcpt_to, expected_message.rcpt_to); - - if let Some(needle) = expected_message.message.strip_prefix('@') { - assert!( - message.message.contains(needle), - "[{}] needle = {:?}", - message.message, - needle - ); - } else { - let message = if expect_dkim { - if message.message.starts_with("DKIM-Signature:") { - message.message.split_once('\n').unwrap().1 - } else { - panic!( - "Expected DKIM-Signature header but got: {}", - message.message - ); - } - } else { - &message.message - }; - - assert_eq!(message, expected_message.message); - } + message } result => { - panic!( - "Timeout waiting for message {:?}: {:?}", - expected_message, result - ); + panic!("Timeout waiting for message, got: {:?}", result); } } } +pub async fn assert_message_delivery( + event_rx: &mut mpsc::Receiver, + expected_message: MockMessage, +) { + let message = expect_message_delivery(event_rx).await; + + assert_eq!(message.mail_from, expected_message.mail_from); + assert_eq!(message.rcpt_to, expected_message.rcpt_to); + + if let Some(needle) = expected_message.message.strip_prefix('@') { + assert!( + message.message.contains(needle), + "[{}] needle = {:?}", + message.message, + needle + ); + } else { + assert!( + message.message.contains(&expected_message.message), + "Got [{}], Expected[{}]", + message.message, + expected_message.message + ); + } +} + pub async fn expect_nothing(event_rx: &mut mpsc::Receiver) { match tokio::time::timeout(Duration::from_millis(500), event_rx.recv()).await { Err(_) => {} diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index b54a1cbe..a0fb6776 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -57,6 +57,10 @@ certificate = "default" [session.ehlo] reject-non-fqdn = false +[session.rcpt] +relay = [ { if = "authenticated-as", ne = "", then = true }, + { else = false } ] + [session.rcpt.lookup] domains = "list/domains" addresses = "local" @@ -69,6 +73,7 @@ wait = "1ms" [list] domains = ["example.com"] +remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"] [queue] path = "{TMP}" @@ -83,7 +88,7 @@ type = "system" [queue.outbound] next-hop = [ { if = "rcpt-domain", in-list = "list/domains", then = "local" }, - { if = "rcpt-domain", eq = "remote.org", then = "mock-smtp" }, + { if = "rcpt-domain", in-list = "list/remote-domains", then = "mock-smtp" }, { else = false } ] [remote."mock-smtp"] @@ -95,6 +100,10 @@ protocol = "smtp" implicit = false allow-invalid-certs = true +[session.extensions] +future-release = [ { if = "authenticated-as", ne = "", then = "99999999d"}, + { else = false } ] + [store] db.path = "{TMP}/sqlite.db" blob.path = "{TMP}" @@ -114,9 +123,9 @@ max-size = 5000000 max-concurrent = 4 [jmap.rate-limit] -account.rate = "100/1m" +account.rate = "1000/1m" authentication.rate = "100/1m" -anonymous.rate = "1000/1m" +anonymous.rate = "100/1m" [jmap.event-source] throttle = "500ms" @@ -183,7 +192,8 @@ pub async fn jmap_tests() { //event_source::test(params.server.clone(), &mut params.client).await; //push_subscription::test(params.server.clone(), &mut params.client).await; //sieve_script::test(params.server.clone(), &mut params.client).await; - vacation_response::test(params.server.clone(), &mut params.client).await; + //vacation_response::test(params.server.clone(), &mut params.client).await; + email_submission::test(params.server.clone(), &mut params.client).await; let websockets = "todo"; diff --git a/tests/src/jmap/sieve_script.rs b/tests/src/jmap/sieve_script.rs index a82abf20..74b8c81c 100644 --- a/tests/src/jmap/sieve_script.rs +++ b/tests/src/jmap/sieve_script.rs @@ -291,7 +291,6 @@ pub async fn test(server: Arc, client: &mut Client) { assert_message_delivery( &mut smtp_rx, MockMessage::new("<>", [""], "@No soup for you"), - false, ) .await; @@ -326,7 +325,6 @@ pub async fn test(server: Arc, client: &mut Client) { [""], "@Rejected from an included script", ), - false, ) .await; @@ -359,7 +357,6 @@ pub async fn test(server: Arc, client: &mut Client) { [""], "@Attached you'll find", ), - false, ) .await; assert_eq!( @@ -399,7 +396,6 @@ pub async fn test(server: Arc, client: &mut Client) { [""], "@It's TPS-o-clock", ), - false, ) .await; diff --git a/tests/src/jmap/vacation_response.rs b/tests/src/jmap/vacation_response.rs index 53c14916..362c9146 100644 --- a/tests/src/jmap/vacation_response.rs +++ b/tests/src/jmap/vacation_response.rs @@ -63,7 +63,6 @@ pub async fn test(server: Arc, client: &mut Client) { assert_message_delivery( &mut smtp_rx, MockMessage::new("", [""], "@Kokomo"), - false, ) .await; @@ -143,7 +142,6 @@ pub async fn test(server: Arc, client: &mut Client) { assert_message_delivery( &mut smtp_rx, MockMessage::new("", [""], "@Kokomo"), - false, ) .await;