diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd0b4bf..511be803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.3.5] - 2023-08-18 + +## Added +- TCP listener option `nodelay`. + +### Changed + +### Fixed +- SMTP: Allow disabling `STARTTLS`. +- JMAP: Support for `OPTIONS` HTTP method. + ## [0.3.4] - 2023-08-09 ## Added diff --git a/Cargo.lock b/Cargo.lock index 39b08235..b3b92a09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "argon2" @@ -254,7 +254,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -276,18 +276,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -498,7 +498,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -531,9 +531,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -824,9 +824,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.21" +version = "4.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +checksum = "b417ae4361bca3f5de378294fc7472d3c4ed86a5ef9f49e93ae722f432aae8d2" dependencies = [ "clap_builder", "clap_derive", @@ -835,9 +835,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.21" +version = "4.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +checksum = "9c90dc0f0e42c64bff177ca9d7be6fcc9ddb0f26a6e062174a61c84dd6c644d4" dependencies = [ "anstream", "anstyle", @@ -854,7 +854,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1079,9 +1079,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.3" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7" +checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2" dependencies = [ "cfg-if", "cpufeatures", @@ -1102,7 +1102,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1347,7 +1347,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1402,9 +1402,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" dependencies = [ "pkcs8", "signature", @@ -1412,9 +1412,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0-rc.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c" +checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ "curve25519-dalek", "ed25519", @@ -1606,9 +1606,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "libz-sys", @@ -1711,7 +1711,7 @@ checksum = "83c8d52fe8b46ab822b4decdcc0d6d85aeedfc98f0d52ba2bd4aec4a97807516" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "try_map", ] @@ -1797,7 +1797,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1857,7 +1857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets 0.48.1", + "windows-targets 0.48.4", ] [[package]] @@ -2073,9 +2073,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human-size" @@ -2233,7 +2233,7 @@ dependencies = [ [[package]] name = "imap" -version = "0.3.4" +version = "0.3.5" dependencies = [ "ahash 0.8.3", "dashmap", @@ -2398,7 +2398,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes", "aes-gcm", @@ -2660,9 +2660,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru-cache" @@ -2723,7 +2723,7 @@ dependencies = [ [[package]] name = "mail-send" version = "0.4.0" -source = "git+https://github.com/stalwartlabs/mail-send#d5ac9b328308fd95709cb8ee1c3ce37716e210ef" +source = "git+https://github.com/stalwartlabs/mail-send#ffa60e3f653d0f4057b7c97d103751a80adc4c12" dependencies = [ "base64 0.20.0", "gethostname", @@ -2732,12 +2732,12 @@ dependencies = [ "smtp-proto", "tokio", "tokio-rustls 0.24.1", - "webpki-roots 0.23.1", + "webpki-roots 0.25.2", ] [[package]] name = "mail-server" -version = "0.3.4" +version = "0.3.5" dependencies = [ "directory", "imap", @@ -2999,7 +2999,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -3102,7 +3102,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -3297,7 +3297,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.4", ] [[package]] @@ -3475,7 +3475,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -3504,7 +3504,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -3593,7 +3593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -3734,9 +3734,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -4101,7 +4101,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4193,11 +4193,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.7" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", @@ -4411,14 +4411,14 @@ checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -4459,7 +4459,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4597,7 +4597,7 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "smtp" -version = "0.3.4" +version = "0.3.5" dependencies = [ "ahash 0.8.3", "blake3", @@ -4823,7 +4823,7 @@ checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" dependencies = [ "atoi", "base64 0.21.2", - "bitflags 2.3.3", + "bitflags 2.4.0", "byteorder", "bytes", "crc", @@ -4865,7 +4865,7 @@ checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" dependencies = [ "atoi", "base64 0.21.2", - "bitflags 2.3.3", + "bitflags 2.4.0", "byteorder", "crc", "dotenvy", @@ -4920,7 +4920,7 @@ dependencies = [ [[package]] name = "stalwart-cli" -version = "0.3.4" +version = "0.3.5" dependencies = [ "clap", "console", @@ -4942,7 +4942,7 @@ dependencies = [ [[package]] name = "stalwart-install" -version = "0.3.4" +version = "0.3.5" dependencies = [ "base64 0.21.2", "clap", @@ -5035,9 +5035,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -5153,22 +5153,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5247,9 +5247,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.30.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3ce25f50619af8b0aec2eb23deebe84249e19e2ddd393a6e16e3300a6dadfd" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", "bytes", @@ -5282,7 +5282,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5467,7 +5467,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5748,7 +5748,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utils" -version = "0.3.4" +version = "0.3.5" dependencies = [ "ahash 0.8.3", "dashmap", @@ -5839,7 +5839,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -5873,7 +5873,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5945,10 +5945,16 @@ dependencies = [ ] [[package]] -name = "whatlang" -version = "0.16.2" +name = "webpki-roots" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c531a2dc4c462b833788be2c07eef4e621d0e9edbd55bf280cc164c1c1aa043" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "whatlang" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdcd0195a5b871e50926da8e881277f36a4621b3220d85092e7b91cc85f6bd9" dependencies = [ "hashbrown 0.12.3", "once_cell", @@ -6005,7 +6011,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.4", ] [[package]] @@ -6023,7 +6029,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.4", ] [[package]] @@ -6043,17 +6049,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "d92ecb8ae0317859f509f17b19adc74b0763b0fa3b085dea8ed01085c8dac222" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.4", + "windows_aarch64_msvc 0.48.4", + "windows_i686_gnu 0.48.4", + "windows_i686_msvc 0.48.4", + "windows_x86_64_gnu 0.48.4", + "windows_x86_64_gnullvm 0.48.4", + "windows_x86_64_msvc 0.48.4", ] [[package]] @@ -6064,9 +6070,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "d14b0ee96970be7108701212f097ce67ca772fd84cb0ffbc86d26a94e77ba929" [[package]] name = "windows_aarch64_msvc" @@ -6076,9 +6082,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "1332277d49f440c8fc6014941e320ee47ededfcce10cb272728470f56cc092c9" [[package]] name = "windows_i686_gnu" @@ -6088,9 +6094,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "d992130ac399d56f02c20564e9975ac5ba08cb25cb832849bbc0d736a101abe5" [[package]] name = "windows_i686_msvc" @@ -6100,9 +6106,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "962e96d0fa4b4773c63977977ea6564f463fb10e34a6e07360428b53ae7a3f71" [[package]] name = "windows_x86_64_gnu" @@ -6112,9 +6118,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "30652a53018a48a9735fbc2986ff0446c37bc8bed0d3f98a0ed4d04cdb80027e" [[package]] name = "windows_x86_64_gnullvm" @@ -6124,9 +6130,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "b5bb3f0331abfe1a95af56067f1e64b3791b55b5373b03869560b6025de809bf" [[package]] name = "windows_x86_64_msvc" @@ -6136,9 +6142,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "bd1df36d9fd0bbe4849461de9b969f765170f4e0f90497d580a235d515722b10" [[package]] name = "winreg" @@ -6170,9 +6176,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-rc.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core", @@ -6235,7 +6241,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/crates/antispam/src/import/meta.rs b/crates/antispam/src/import/meta.rs index b85c95b3..199a1f56 100644 --- a/crates/antispam/src/import/meta.rs +++ b/crates/antispam/src/import/meta.rs @@ -1,6 +1,12 @@ -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, iter::Peekable, str::Chars}; -use super::Token; +use super::{Comparator, Logical, Operation, Token}; + +// Parse a meta expression into a list of tokens that can be easily +// converted into a Sieve test. +// The parser is not very robust but works on all SpamAssassin meta expressions. +// It might be a good idea in the future to instead build a parse tree and +// then convert that into a Sieve expression. #[derive(Debug, Clone, Default)] pub struct MetaExpression { @@ -11,7 +17,7 @@ pub struct MetaExpression { #[derive(Debug, Clone)] pub struct TokenDepth { - token: Token, + pub token: Token, depth: u32, prefix: Vec, } @@ -40,15 +46,9 @@ impl MetaExpression { if !buf.is_empty() { let token = Token::from(buf); buf = String::new(); - if !seen_comp - && matches!( - iter.clone() - .find(|t| { ['&', '|', '>', '<', '='].contains(t) }), - None | Some('&' | '|') - ) - { + if !seen_comp && !meta.has_comparator(iter.clone()) { meta.push(token); - meta.push(Token::Gt); + meta.push(Token::Comparator(Comparator::Gt)); meta.push(Token::Number(0)); seen_comp = true; } else { @@ -60,7 +60,7 @@ impl MetaExpression { '&' => { seen_comp = false; if matches!(iter.next(), Some('&')) { - meta.push(Token::And); + meta.push(Token::Logical(Logical::And)); } else { eprintln!("Warning: Single & in meta expression {expr}",); } @@ -68,24 +68,24 @@ impl MetaExpression { '|' => { seen_comp = false; if matches!(iter.next(), Some('|')) { - meta.push(Token::Or); + meta.push(Token::Logical(Logical::Or)); } else { eprintln!("Warning: Single | in meta expression {expr}",); } } '!' => { seen_comp = false; - meta.push(Token::Not) + meta.push(Token::Logical(Logical::Not)) } '=' => { seen_comp = true; meta.push(match iter.next() { - Some('=') => Token::Eq, - Some('>') => Token::Ge, - Some('<') => Token::Le, + Some('=') => Token::Comparator(Comparator::Eq), + Some('>') => Token::Comparator(Comparator::Ge), + Some('<') => Token::Comparator(Comparator::Le), _ => { eprintln!("Warning: Single = in meta expression {expr}",); - Token::Eq + Token::Comparator(Comparator::Eq) } }); } @@ -94,9 +94,9 @@ impl MetaExpression { meta.push(match iter.peek() { Some('=') => { iter.next(); - Token::Ge + Token::Comparator(Comparator::Ge) } - _ => Token::Gt, + _ => Token::Comparator(Comparator::Gt), }) } '<' => { @@ -104,9 +104,9 @@ impl MetaExpression { meta.push(match iter.peek() { Some('=') => { iter.next(); - Token::Le + Token::Comparator(Comparator::Le) } - _ => Token::Lt, + _ => Token::Comparator(Comparator::Lt), }) } '(' => meta.push(Token::OpenParen), @@ -119,9 +119,9 @@ impl MetaExpression { meta.push(Token::CloseParen) } - '+' => meta.push(Token::Add), - '*' => meta.push(Token::Multiply), - '/' => meta.push(Token::Divide), + '+' => meta.push(Token::Operation(Operation::Add)), + '*' => meta.push(Token::Operation(Operation::Multiply)), + '/' => meta.push(Token::Operation(Operation::Divide)), ' ' => {} _ => { eprintln!("Warning: Invalid character {ch} in meta expression {expr}"); @@ -139,7 +139,7 @@ impl MetaExpression { if !buf.is_empty() { meta.push(Token::from(buf)); if !seen_comp { - meta.push(Token::Gt); + meta.push(Token::Comparator(Comparator::Gt)); meta.push(Token::Number(0)); } } @@ -148,7 +148,7 @@ impl MetaExpression { meta } - fn push(&mut self, token: Token) { + fn push(&mut self, mut token: Token) { let pos = self.tokens.len(); let depth_range = self .depth_range @@ -182,35 +182,60 @@ impl MetaExpression { self.depth = self.depth.saturating_sub(1); depth = self.depth; } - Token::Or | Token::And => { - let start_prefix = &mut self.tokens[depth_range.start].prefix; - if !start_prefix.contains(&Token::And) && !start_prefix.contains(&Token::Or) { - start_prefix.insert(0, token.clone()); - } - depth_range.logic_end = true; - if let Some((pos, is_static)) = depth_range.expr_end.take() { - self.tokens[pos + 2] - .prefix - .push(Token::BeginExpression(is_static)); - prefix.push(Token::EndExpression(is_static)); + Token::Logical(op) => { + if self + .tokens + .iter() + .any(|t| matches!(t.token, Token::Comparator(_)) && t.depth < depth) + { + token = Token::Operation(match op { + Logical::And => Operation::And, + Logical::Or => Operation::Or, + Logical::Not => Operation::Not, + }); + if let Some((pos, true)) = depth_range.expr_end { + depth_range.expr_end = Some((pos, false)); + } + } else if matches!(op, Logical::Or | Logical::And) { + let start_prefix = &mut self.tokens[depth_range.start].prefix; + if !start_prefix.contains(&Token::Logical(Logical::And)) + && !start_prefix.contains(&Token::Logical(Logical::Or)) + { + start_prefix.insert(0, token.clone()); + } + depth_range.logic_end = true; + if let Some((pos, is_static)) = depth_range.expr_end.take() { + self.tokens[pos + 2] + .prefix + .push(Token::BeginExpression(is_static)); + prefix.push(Token::EndExpression(is_static)); + } } } - Token::Lt | Token::Gt | Token::Eq | Token::Le | Token::Ge => { + Token::Comparator(_) => { let mut is_static = true; let mut start_pos = usize::MAX; - for (pos, token) in self.tokens.iter().enumerate().rev() { + for (pos, token) in self.tokens.iter_mut().enumerate().rev() { if token.depth >= depth { start_pos = pos; match &token.token { - Token::And | Token::Or | Token::Not => { - start_pos += 1; - break; + Token::Logical(op) => { + if token.depth == depth { + start_pos += 1; + break; + } else { + is_static = false; + token.token = Token::Operation(match op { + Logical::And => Operation::And, + Logical::Or => Operation::Or, + Logical::Not => Operation::Not, + }); + token.prefix.clear(); + } } Token::OpenParen | Token::CloseParen - | Token::Add - | Token::Multiply - | Token::Divide + | Token::Operation(_) | Token::Tag(_) => { is_static = false; } @@ -231,7 +256,7 @@ impl MetaExpression { depth_range.expr_end = Some((pos, true)); } } - Token::Tag(_) | Token::Add | Token::Multiply | Token::Divide => { + Token::Tag(_) | Token::Operation(_) => { if let Some((pos, true)) = depth_range.expr_end { depth_range.expr_end = Some((pos, false)); } @@ -266,6 +291,47 @@ impl MetaExpression { } } } + + fn has_comparator(&self, iter: Peekable>) -> bool { + let mut d = self.depth; + let mut comp_depth = None; + let mut logic_depth = None; + + for (pos, ch) in iter.enumerate() { + match ch { + '(' => { + d += 1; + } + ')' => { + d = d.saturating_sub(1); + } + '>' | '<' | '=' => { + comp_depth = Some((pos, d)); + break; + } + '&' | '|' => { + if d <= self.depth { + logic_depth = Some((pos, d)); + } + } + _ => (), + } + } + + println!("comp_depth: {comp_depth:?} {logic_depth:?}"); + + match (comp_depth, logic_depth) { + (Some((comp_pos, comp_depth)), Some((logic_pos, logic_depth))) => { + match comp_depth.cmp(&logic_depth) { + std::cmp::Ordering::Less => true, + std::cmp::Ordering::Equal => comp_pos < logic_pos, + _ => false, + } + } + (Some(_), None) => true, + _ => false, + } + } } impl From for Token { @@ -288,8 +354,12 @@ impl Display for MetaExpression { } match &token.token { - Token::And | Token::Or => f.write_str(", "), - Token::Gt | Token::Lt | Token::Eq | Token::Ge | Token::Le => f.write_str(" "), + Token::Logical(Logical::And) | Token::Logical(Logical::Or) => f.write_str(", "), + Token::Comparator(Comparator::Gt) + | Token::Comparator(Comparator::Lt) + | Token::Comparator(Comparator::Eq) + | Token::Comparator(Comparator::Ge) + | Token::Comparator(Comparator::Le) => f.write_str(" "), _ => token.token.fmt(f), }?; } @@ -303,27 +373,30 @@ impl Display for Token { match self { Token::Tag(t) => t.fmt(f), Token::Number(n) => n.fmt(f), - Token::And => f.write_str("allof("), - Token::Or => f.write_str("anyof("), - Token::Not => f.write_str("not "), - Token::Lt | Token::Eq | Token::Ge | Token::Le | Token::Gt => { - f.write_str("string :")?; - match self { - Token::Eq => f.write_str("eq")?, - Token::Gt => f.write_str("gt")?, - Token::Lt => f.write_str("lt")?, - Token::Ge => f.write_str("ge")?, - Token::Le => f.write_str("gt")?, + Token::Logical(Logical::And) => f.write_str("allof("), + Token::Logical(Logical::Or) => f.write_str("anyof("), + Token::Logical(Logical::Not) => f.write_str("not "), + Token::Comparator(comp) => { + f.write_str("string :value \"")?; + match comp { + Comparator::Eq => f.write_str("eq")?, + Comparator::Gt => f.write_str("gt")?, + Comparator::Lt => f.write_str("lt")?, + Comparator::Ge => f.write_str("ge")?, + Comparator::Le => f.write_str("gt")?, _ => unreachable!(), } - f.write_str(" ") + f.write_str("\" :comparator \"i;ascii-numeric\" ") } Token::OpenParen => f.write_str("("), Token::CloseParen => f.write_str(")"), - Token::Add => f.write_str(" + "), - Token::Multiply => f.write_str(" * "), - Token::Divide => f.write_str(" / "), + Token::Operation(Operation::Add) => f.write_str(" + "), + Token::Operation(Operation::Multiply) => f.write_str(" * "), + Token::Operation(Operation::Divide) => f.write_str(" / "), + Token::Operation(Operation::And) => f.write_str(" & "), + Token::Operation(Operation::Or) => f.write_str(" | "), + Token::Operation(Operation::Not) => f.write_str("!"), Token::BeginExpression(is_static) => { if *is_static { f.write_str("\"") @@ -363,17 +436,19 @@ mod test { ("__ML2 || __ML4", ""), ("(__AT_HOTMAIL_MSGID && (!__FROM_HOTMAIL_COM && !__FROM_MSN_COM && !__FROM_YAHOO_COM))", ""), ("(0)", ""), - ("RAZOR2_CHECK + DCC_CHECK + PYZOR_CHECK > 1", ""),*/ + ("RAZOR2_CHECK + DCC_CHECK + PYZOR_CHECK > 1", ""), + ("(SUBJECT_IN_BLOCKLIST)", ""), ("__HAS_MSGID && !(__SANE_MSGID || __MSGID_COMMENT)", ""), ("!__CTYPE_HTML && __X_MAILER_APPLEMAIL && (__MSGID_APPLEMAIL || __MIME_VERSION_APPLEMAIL)", ""), - ("((__AUTO_GEN_MS||__AUTO_GEN_3||__AUTO_GEN_4) && !__XM_VBULLETIN && !__X_CRON_ENV)", ""), + ("((__AUTO_GEN_MS||__AUTO_GEN_3||__AUTO_GEN_4) && !__XM_VBULLETIN && !__X_CRON_ENV)", ""),*/ + ("(__WEBMAIL_ACCT + __MAILBOX_FULL + (__TVD_PH_SUBJ_META || __TVD_PH_BODY_META) > 3)", ""), ] { let meta = MetaExpression::from_meta(expr); //println!("{:#?}", meta.tokens); let result = meta.to_string(); - //println!("{}", expected); + println!("{expr}"); println!("{}", result); /*assert_eq!( diff --git a/crates/antispam/src/import/mod.rs b/crates/antispam/src/import/mod.rs index 052cd55f..f71cdac0 100644 --- a/crates/antispam/src/import/mod.rs +++ b/crates/antispam/src/import/mod.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; +use self::meta::MetaExpression; + pub mod meta; pub mod spamassassin; pub mod utils; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] struct Rule { name: String, t: RuleType, @@ -12,13 +14,16 @@ struct Rule { description: HashMap, priority: i32, flags: Vec, + forward_score_pos: f64, + forward_score_neg: f64, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] enum RuleType { Header { matches: HeaderMatches, header: Header, + part: Vec, if_unset: Option, pattern: String, }, @@ -37,7 +42,7 @@ enum RuleType { params: Vec, }, Meta { - tokens: Vec, + expr: MetaExpression, }, #[default] @@ -56,7 +61,7 @@ impl RuleType { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone)] enum TestFlag { Net, Nice, @@ -74,7 +79,7 @@ enum TestFlag { DnsBlockRule(String), } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] enum Header { #[default] All, @@ -82,13 +87,10 @@ enum Header { AllExternal, EnvelopeFrom, ToCc, - Name { - name: String, - part: Vec, - }, + Name(String), } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] enum HeaderMatches { #[default] Matches, @@ -96,7 +98,7 @@ enum HeaderMatches { Exists, } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] enum HeaderPart { Name, Addr, @@ -108,23 +110,42 @@ enum HeaderPart { pub enum Token { Tag(String), Number(u32), + Logical(Logical), + Comparator(Comparator), + Operation(Operation), + + OpenParen, + CloseParen, + + // Sieve specific + BeginExpression(bool), + EndExpression(bool), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Logical { And, Or, Not, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Comparator { Gt, Lt, Eq, Ge, Le, - OpenParen, - CloseParen, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Operation { Add, Multiply, Divide, - - // Sieve specific - BeginExpression(bool), - EndExpression(bool), + And, + Or, + Not, } impl Rule { @@ -143,12 +164,39 @@ impl Rule { impl Ord for Rule { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.priority.cmp(&other.priority) { - std::cmp::Ordering::Equal => match self.score().partial_cmp(&other.score()).unwrap() { - std::cmp::Ordering::Equal => other.name.cmp(&self.name), + let this_score = self.score(); + let other_score = other.score(); + + let this_is_negative = this_score < 0.0; + let other_is_negative = other_score < 0.0; + + if this_is_negative != other_is_negative { + if this_is_negative { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + } + } else { + let this_priority = if this_score != 0.0 { + self.priority + } else { + 9000 + }; + let other_priority = if other_score != 0.0 { + other.priority + } else { + 9000 + }; + + match this_priority.cmp(&other_priority) { + std::cmp::Ordering::Equal => { + match other_score.abs().partial_cmp(&this_score.abs()).unwrap() { + std::cmp::Ordering::Equal => other.name.cmp(&self.name), + x => x, + } + } x => x, - }, - x => x, + } } } } diff --git a/crates/antispam/src/import/spamassassin.rs b/crates/antispam/src/import/spamassassin.rs index f6fa3e99..706c75dd 100644 --- a/crates/antispam/src/import/spamassassin.rs +++ b/crates/antispam/src/import/spamassassin.rs @@ -1,8 +1,7 @@ use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - default, - fmt::format, - fs, + collections::{BTreeMap, HashMap, HashSet}, + fmt::{Display, Write}, + fs::{self}, path::PathBuf, }; @@ -12,7 +11,9 @@ use super::{ Header, HeaderMatches, HeaderPart, Rule, RuleType, TestFlag, Token, UnwrapResult, }; -static SUPPORTED_PLUGINS: [&str; 37] = [ +const VERSION: f64 = 4.000000; + +static IF_TRUE: [&str; 57] = [ "Mail::SpamAssassin::Plugin::DKIM", "Mail::SpamAssassin::Plugin::SPF", "Mail::SpamAssassin::Plugin::ASN", @@ -49,9 +50,31 @@ static SUPPORTED_PLUGINS: [&str; 37] = [ "Mail::SpamAssassin::Plugin::VBounce", "Mail::SpamAssassin::Plugin::WLBLEval", "Mail::SpamAssassin::Plugin::WelcomeListSubject", - "Mail::SpamAssassin::Plugin::WhiteListSubject", + "Mail::SpamAssassin::Conf::feature_bayes_stopwords", + "Mail::SpamAssassin::Conf::feature_bug6558_free", + "Mail::SpamAssassin::Conf::feature_capture_rules", + "Mail::SpamAssassin::Conf::feature_dns_local_ports_permit_avoid", + "Mail::SpamAssassin::Conf::feature_originating_ip_headers", + "Mail::SpamAssassin::Conf::feature_registryboundaries", + "Mail::SpamAssassin::Conf::feature_welcomelist_blocklist", + "Mail::SpamAssassin::Conf::feature_yesno_takes_args", + "Mail::SpamAssassin::Conf::perl_min_version_5010000", + "Mail::SpamAssassin::Plugin::BodyEval::has_check_body_length", + "Mail::SpamAssassin::Plugin::DKIM::has_arc", + "Mail::SpamAssassin::Plugin::DecodeShortURLs::has_get", + "Mail::SpamAssassin::Plugin::DecodeShortURLs::has_short_url_redir", + "Mail::SpamAssassin::Plugin::MIMEEval::has_check_abundant_unicode_ratio", + "Mail::SpamAssassin::Plugin::MIMEEval::has_check_for_ascii_text_illegal", + "Mail::SpamAssassin::Plugin::SPF::has_check_for_spf_errors", + "Mail::SpamAssassin::Plugin::URIDNSBL::has_tflags_domains_only", + "Mail::SpamAssassin::Plugin::URIDNSBL::has_uridnsbl_for_a", + "Mail::SpamAssassin::Plugin::ASN::has_check_asn", + "Mail::SpamAssassin::Conf::compat_welcomelist_blocklist", + "Mail::SpamAssassin::Conf::feature_dns_block_rule", ]; +static IF_FALSE: [&str; 1] = ["Mail::SpamAssassin::Plugin::WhiteListSubject"]; + static SUPPORTED_FUNCTIONS: [&str; 162] = [ "check_abundant_unicode_ratio", "check_access_database", @@ -217,59 +240,6 @@ static SUPPORTED_FUNCTIONS: [&str; 162] = [ "tvd_vertical_words", ]; -static IF_TRUE: [&str; 25] = [ - "!(!plugin(Mail::SpamAssassin::Plugin::DKIM))", - "(version >= 3.003000)", - "(version >= 3.004000)", - "(version >= 3.004001)", - "(version >= 3.004002)", - "(version >= 3.004003)", - "(version >= 4.000000)", - "can(Mail::SpamAssassin::Conf::feature_bayes_stopwords)", - "can(Mail::SpamAssassin::Conf::feature_bug6558_free)", - "can(Mail::SpamAssassin::Conf::feature_capture_rules)", - "can(Mail::SpamAssassin::Conf::feature_dns_local_ports_permit_avoid)", - "can(Mail::SpamAssassin::Conf::feature_originating_ip_headers)", - "can(Mail::SpamAssassin::Conf::feature_registryboundaries)", - "can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)", - "can(Mail::SpamAssassin::Conf::feature_yesno_takes_args)", - "can(Mail::SpamAssassin::Conf::perl_min_version_5010000)", - "can(Mail::SpamAssassin::Plugin::BodyEval::has_check_body_length)", - "can(Mail::SpamAssassin::Plugin::DKIM::has_arc)", - "can(Mail::SpamAssassin::Plugin::DecodeShortURLs::has_get)", - "can(Mail::SpamAssassin::Plugin::DecodeShortURLs::has_short_url_redir)", - "can(Mail::SpamAssassin::Plugin::MIMEEval::has_check_abundant_unicode_ratio)", - "can(Mail::SpamAssassin::Plugin::MIMEEval::has_check_for_ascii_text_illegal)", - "can(Mail::SpamAssassin::Plugin::SPF::has_check_for_spf_errors)", - "can(Mail::SpamAssassin::Plugin::URIDNSBL::has_tflags_domains_only)", - "can(Mail::SpamAssassin::Plugin::URIDNSBL::has_uridnsbl_for_a)", -]; - -static IF_FALSE: [&str; 22] = [ - "(version < 4.000000)", - "!((version >= 3.003000))", - "!((version >= 3.004000))", - "can(Mail::SpamAssassin::Conf::feature_dns_block_rule)", - "!plugin(Mail::SpamAssassin::Plugin::BodyEval)", - "!plugin(Mail::SpamAssassin::Plugin::DKIM)", - "!plugin(Mail::SpamAssassin::Plugin::FreeMail)", - "!plugin(Mail::SpamAssassin::Plugin::HTMLEval)", - "!plugin(Mail::SpamAssassin::Plugin::HeaderEval)", - "!plugin(Mail::SpamAssassin::Plugin::ImageInfo)", - "!plugin(Mail::SpamAssassin::Plugin::MIMEEval)", - "!plugin(Mail::SpamAssassin::Plugin::MIMEHeader)", - "!plugin(Mail::SpamAssassin::Plugin::ReplaceTags)", - "!plugin(Mail::SpamAssassin::Plugin::SPF)", - "!plugin(Mail::SpamAssassin::Plugin::WLBLEval)", - "!plugin(Mail::SpamAssassin::Plugin::WelcomeListSubject)", - "!(can(Mail::SpamAssassin::Conf::feature_bug6558_free))", - "!(can(Mail::SpamAssassin::Plugin::ASN::has_check_asn))", - "!(can(Mail::SpamAssassin::Plugin::BodyEval::has_check_body_length))", - "!can(Mail::SpamAssassin::Conf::compat_welcomelist_blocklist)", - "!can(Mail::SpamAssassin::Conf::feature_welcomelist_blocklist)", - "!can(Mail::SpamAssassin::Plugin::DecodeShortURLs::has_short_url_redir)", -]; - pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, validate_regex: bool) { let mut paths: Vec<_> = fs::read_dir(&path) .unwrap_result("read directory") @@ -285,7 +255,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali let mut replace_rules: HashSet = HashSet::new(); let mut tags: HashMap = HashMap::new(); - let mut unsupported_plugins: BTreeMap>> = BTreeMap::new(); + let mut unsupported_ifs: BTreeMap>> = BTreeMap::new(); let mut unsupported_commands: BTreeMap>> = BTreeMap::new(); for path in paths { @@ -345,7 +315,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali last_ch = ch; } - let (cmd, params) = line + let (cmd, mut params) = line .split_once(' ') .map(|(k, v)| (k.trim(), v.trim())) .unwrap_or((line.as_str().trim(), "")); @@ -358,10 +328,10 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali match cmd { "ifplugin" => { is_supported_stack.push(is_supported_block); - is_supported_block = SUPPORTED_PLUGINS.contains(¶ms); + is_supported_block = IF_TRUE.contains(¶ms); - if !is_supported_block { - unsupported_plugins + if !is_supported_block && !IF_FALSE.contains(¶ms) { + unsupported_ifs .entry(params.to_string()) .or_default() .entry(path.clone()) @@ -370,14 +340,79 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali } } "if" => { - is_supported_stack.push(is_supported_block); - is_supported_block = IF_TRUE.contains(¶ms); - if !is_supported_block && !IF_FALSE.contains(¶ms) { - eprintln!( - "Warning: Unknown if condition on {}, line {}", - path.display(), - line_num - ); + let _params = params; + let mut is_not = false; + loop { + let mut has_changes = false; + if let Some(expr) = params.strip_prefix('!') { + is_not = !is_not; + params = expr.trim(); + has_changes = true; + } + if let Some(expr) = + params.strip_prefix('(').and_then(|v| v.strip_suffix(')')) + { + params = expr.trim(); + has_changes = true; + } + if let Some(expr) = params + .strip_prefix("can(") + .or_else(|| params.strip_prefix("plugin(")) + .and_then(|v| v.strip_suffix(')')) + { + params = expr.trim(); + has_changes = true; + } + if !has_changes { + break; + } + } + + if let Some(version) = params.strip_prefix("version ") { + is_supported_stack.push(is_supported_block); + let (op, version) = version.trim().split_once(' ').unwrap_or(("", version)); + let version = version + .parse::() + .unwrap_result("Failed to parse version"); + match op { + "<" => { + is_supported_block = (VERSION < version) ^ is_not; + } + "<=" => { + is_supported_block = (VERSION <= version) ^ is_not; + } + ">" => { + is_supported_block = (VERSION > version) ^ is_not; + } + ">=" => { + is_supported_block = (VERSION >= version) ^ is_not; + } + "==" => { + is_supported_block = (VERSION == version) ^ is_not; + } + "!=" => { + is_supported_block = (VERSION != version) ^ is_not; + } + _ => { + eprintln!( + "Warning: Invalid version operator on {}, line {}", + path.display(), + line_num + ); + } + } + } else { + is_supported_stack.push(is_supported_block); + is_supported_block = IF_TRUE.contains(¶ms); + if !is_supported_block && !IF_FALSE.contains(¶ms) { + unsupported_ifs + .entry(params.to_string()) + .or_default() + .entry(path.clone()) + .or_default() + .push(line_num.to_string()); + } + is_supported_block ^= is_not; } } "endif" => { @@ -410,7 +445,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali if let Some((name, value)) = params.split_once(' ').map(|(k, v)| (k.trim(), v.trim())) { - let mut rule = rules.entry(name.to_string()).or_default(); + let rule = rules.entry(name.to_string()).or_default(); if let Some(function) = value.strip_prefix("eval:") { if let Some((fnc_name, params_)) = function @@ -478,17 +513,33 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali if let Some(exists) = value.strip_prefix("exists:") { rule.t = RuleType::Header { matches: HeaderMatches::Exists, - header: Header::Name { - name: exists.to_string(), - part: vec![], - }, + header: Header::Name(exists.to_string()), if_unset: None, pattern: String::new(), + part: vec![], }; } else if let Some((header, (op, mut pattern))) = value .split_once(' ') .and_then(|(k, v)| (k.trim(), v.trim().split_once(' ')?).into()) { + let (header, part) = header.split_once(':').unwrap_or((header, "")); + let part = part.split(':').filter_map(|part| { + match part.trim() { + "name" => {Some(HeaderPart::Name)} + "addr" => {Some(HeaderPart::Addr)} + "raw" => {Some(HeaderPart::Raw)} + "" => None, + _ => { + eprintln!( + "Warning: Invalid header part {part:?} on {}, line {}", + path.display(), + line_num + ); + None + } + } + + }).collect::>(); rule.t = RuleType::Header { matches: match op { "=~" => HeaderMatches::Matches, @@ -502,38 +553,13 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali continue; } }, - header: if let Some((header, part)) = header.split_once(':') { - Header::Name { - name: header.to_string(), - part: part.split(':').filter_map(|part| { - match part { - "name" => {Some(HeaderPart::Name)} - "addr" => {Some(HeaderPart::Addr)} - "raw" => {Some(HeaderPart::Raw)} - _ => { - eprintln!( - "Warning: Invalid header part {part:?} on {}, line {}", - path.display(), - line_num - ); - None - } - } - - }).collect::>() - } - } else { - match header { - "ALL" => Header::All, - "MESSAGEID" => Header::MessageId, - "ALL-EXTERNAL" => Header::AllExternal, - "EnvelopeFrom" => Header::EnvelopeFrom, - "ToCc" => Header::ToCc, - _ => Header::Name { - name: header.to_string(), - part: vec![], - }, - } + header: match header { + "ALL" => Header::All, + "MESSAGEID" => Header::MessageId, + "ALL-EXTERNAL" => Header::AllExternal, + "EnvelopeFrom" => Header::EnvelopeFrom, + "ToCc" => Header::ToCc, + _ => Header::Name(header.to_string()), }, if_unset: pattern.rsplit_once("[if-unset:").and_then( |(new_pattern, if_unset)| { @@ -553,6 +579,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali }, ), pattern: fix_broken_regex(pattern).to_string(), + part, }; } else { eprintln!( @@ -620,7 +647,7 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali } "meta" => { if let Some((test_name, expression)) = params.split_once(' ') { - let tokens = MetaExpression::from_meta(expression); + let expr = MetaExpression::from_meta(expression); /*if tokens.tokens.contains(&Token::Divide) { println!( "->: {expression}\n{:?}\n<-: {}", @@ -632,10 +659,8 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali String::from(tokens.clone()) ); std::process::exit(1); - } - rules.entry(test_name.to_string()).or_default().t = RuleType::Meta { - tokens: tokens.tokens, - };*/ + }*/ + rules.entry(test_name.to_string()).or_default().t = RuleType::Meta { expr }; } else { eprintln!( "Warning: Invalid meta command on {}, line {}", @@ -993,53 +1018,51 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali } }) .collect::>(); - rules.sort_unstable_by(|a, b| b.cmp(a)); + rules.sort_unstable(); - let no_meta: Vec = vec![]; + let no_meta = MetaExpression::default(); let mut meta = &no_meta; let mut tests_done = HashSet::new(); + let mut tests_linked = HashSet::new(); let mut rules_iter = rules.iter(); let mut rules_stack = Vec::new(); + let mut rules_sorted = Vec::with_capacity(rules.len()); + // Sort rules by meta loop { while let Some(rule) = rules_iter.next() { + let in_meta = !meta.tokens.is_empty(); if tests_done.contains(&rule.name) - || (!meta.is_empty() + || (in_meta && !meta + .tokens .iter() - .any(|t| matches!(t, Token::Tag(n) if n == &rule.name))) + .any(|t| matches!(&t.token, Token::Tag(n) if n == &rule.name))) { continue; } + tests_done.insert(&rule.name); + if in_meta { + tests_linked.insert(&rule.name); + } match &rule.t { - RuleType::Meta { tokens } => { - meta = tokens; - rules_stack.push((meta, rules_iter)); + RuleType::Meta { expr } if rule.score() != 0.0 => { + rules_stack.push((meta, rule, rules_iter)); rules_iter = rules.iter(); + meta = expr; + } + _ => { + rules_sorted.push(rule); + //write!(&mut script, "{rule}").unwrap(); } - RuleType::Header { - matches, - header, - if_unset, - pattern, - } => todo!(), - RuleType::Body { pattern, raw } => todo!(), - RuleType::Full { pattern } => todo!(), - RuleType::Uri { pattern } => todo!(), - RuleType::Eval { function, params } => todo!(), - RuleType::None => (), } - - tests_done.insert(&rule.name); } - if let Some((prev_meta, prev_rules_iter)) = rules_stack.pop() { - for token in meta { - //TODO - } - + if let Some((prev_meta, prev_rule, prev_rules_iter)) = rules_stack.pop() { + rules_sorted.push(prev_rule); + //write!(&mut script, "{prev_rule}").unwrap(); rules_iter = prev_rules_iter; meta = prev_meta; } else { @@ -1047,9 +1070,48 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali } } + // Generate script + let mut script = String::new(); + let mut rules_iter = rules_sorted.iter(); + + while let Some(&rule) = rules_iter.next() { + if rule.score() == 0.0 && !tests_linked.contains(&rule.name) { + if do_warn { + eprintln!("Warning: Test {} is never linked to.", rule.name); + } + continue; + } + + // Calculate forward scores + let (score_pos, score_neg) = + rules_iter + .clone() + .fold((0.0, 0.0), |(acc_pos, acc_neg), rule| { + let score = rule.score(); + if score > 0.0 { + (acc_pos + score, acc_neg) + } else if score < 0.0 { + (acc_pos, acc_neg + score) + } else { + (acc_pos, acc_neg) + } + }); + let mut rule = rule.clone(); + rule.forward_score_neg = score_neg; + rule.forward_score_pos = score_pos; + + write!(&mut script, "{rule}").unwrap(); + } + + fs::write( + "/Users/me/code/mail-server/_ignore/script.sieve", + script.as_bytes(), + ) + .unwrap(); + for (message, unsupported) in [ ("commands", unsupported_commands), - ("plugins", unsupported_plugins), + ("plugins", unsupported_ifs), ] { if !unsupported.is_empty() { eprintln!("Unsupported {}:", message); @@ -1071,3 +1133,158 @@ pub fn import_spamassassin(path: PathBuf, extension: String, do_warn: bool, vali } } } + +impl Display for Rule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Add comment + self.description + .get("en") + .map(|v| { + writeln!(f, "# {v} (rank {})", self.priority).unwrap(); + }) + .unwrap_or_else(|| writeln!(f, "# {} (rank {})", self.name, self.priority).unwrap()); + + match &self.t { + RuleType::Header { + matches, + header: header @ (Header::All | Header::AllExternal), + if_unset, + pattern, + part, + } => { + write!( + f, + "if vnd.stalwart.eval(\"match_all_headers\", \"{}\", {:?})", + if header == &Header::All { + "all" + } else { + "all-external" + }, + pattern + )?; + } + RuleType::Header { + matches, + header, + if_unset, + pattern, + part, + } => { + f.write_str("if ")?; + let cmd = if matches!(header, Header::EnvelopeFrom) { + "envelope" + } else if part.contains(&HeaderPart::Addr) || part.contains(&HeaderPart::Name) { + "address" + } else { + "header" + }; + match matches { + HeaderMatches::Matches => write!(f, "{cmd} :regex ")?, + HeaderMatches::NotMatches => write!(f, "not {cmd} :regex ")?, + HeaderMatches::Exists => write!(f, "{cmd} :contains ")?, + } + for part in part { + match part { + HeaderPart::Name => f.write_str(":name ")?, + HeaderPart::Addr => f.write_str(":all ")?, + HeaderPart::Raw => f.write_str(":raw ")?, + } + } + match header { + Header::MessageId => f.write_str("[\"Message-Id\",\"Resent-Message-Id\",\"X-Message-Id\",\"X-Original-Message-ID\"]")?, + Header::ToCc => f.write_str("[\"To\",\"Cc\"]")?, + Header::Name (name) => write!(f, "{:?}", name)?, + Header::EnvelopeFrom => f.write_str("\"from\"")?, + Header::All | + Header::AllExternal => unreachable!(), + } + + write!(f, " {:?}", pattern)?; + } + RuleType::Body { pattern, raw } => { + if *raw { + write!(f, "if body :raw :regex {pattern:?}")?; + } else if !self.flags.contains(&TestFlag::NoSubject) { + write!(f, "if body :subject :regex {pattern:?}")?; + } else { + write!(f, "if body :regex {pattern:?}")?; + } + } + RuleType::Full { pattern } => { + write!(f, "if vnd.stalwart.eval(\"match_full\", {:?})", pattern)?; + } + RuleType::Uri { pattern } => { + write!(f, "if vnd.stalwart.eval(\"match_uri\", {:?})", pattern)?; + } + RuleType::Eval { function, params } => { + write!(f, "if vnd.stalwart.eval({function:?}")?; + for param in params { + write!(f, ", {param:?}")?; + } + f.write_str(")")?; + } + RuleType::Meta { expr } => { + expr.fmt(f)?; + } + RuleType::None => { + f.write_str("if false")?; + } + } + + f.write_str(" {\n\tset \"")?; + f.write_str(&self.name)?; + f.write_str("\" \"1\";\n")?; + let score = self.score(); + + if score != 0.0 { + f.write_str("\tset \"score\" \"${score")?; + if score > 0.0 { + f.write_str(" + ")?; + score.fmt(f)?; + } else { + f.write_str(" - ")?; + (-score).fmt(f)?; + } + f.write_str("}\";\n\t")?; + + if score > 0.0 { + if self.forward_score_neg != 0.0 { + write!( + f, + concat!( + "if allof(string :value \"ge\" :comparator ", + "\"i;ascii-numeric\" \"${{score}}\" \"${{spam_score}}\", ", + "string :value \"ge\" :comparator ", + "\"i;ascii-numeric\" \"${{score - {:.4}}}\" \"${{spam_score}}\")" + ), + -self.forward_score_neg + )?; + } else { + f.write_str(concat!( + "if string :value \"ge\" :comparator ", + "\"i;ascii-numeric\" \"${score}\" \"${spam_score}\"" + ))?; + } + } else if self.forward_score_pos != 0.0 { + write!( + f, + concat!( + "if allof(string :value \"lt\" :comparator ", + "\"i;ascii-numeric\" \"${{score}}\" \"${{spam_score}}\", ", + "string :value \"lt\" :comparator ", + "\"i;ascii-numeric\" \"${{score + {:.4}}}\" \"${{spam_score}}\")" + ), + self.forward_score_pos + )?; + } else { + f.write_str(concat!( + "if string :value \"lt\" :comparator ", + "\"i;ascii-numeric\" \"${score}\" \"${spam_score}\"" + ))?; + } + f.write_str(" {\n\t\treturn;\n\t}\n")?; + } + + f.write_str("}\n\n") + } +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 99402c3b..c5b9a7f0 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only" repository = "https://github.com/stalwartlabs/cli" homepage = "https://github.com/stalwartlabs/cli" -version = "0.3.4" +version = "0.3.5" edition = "2021" readme = "README.md" resolver = "2" diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 95b6d233..0d2d8928 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.3.4" +version = "0.3.5" edition = "2021" resolver = "2" diff --git a/crates/install/Cargo.toml b/crates/install/Cargo.toml index 03b827f9..95b65d71 100644 --- a/crates/install/Cargo.toml +++ b/crates/install/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only" repository = "https://github.com/stalwartlabs/mail-server" homepage = "https://github.com/stalwartlabs/mail-server" -version = "0.3.4" +version = "0.3.5" edition = "2021" readme = "README.md" resolver = "2" diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 3f99a532..afaabd13 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.3.4" +version = "0.3.5" edition = "2021" resolver = "2" diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index fcce341a..7e38d0b2 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["imap", "jmap", "smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only" -version = "0.3.4" +version = "0.3.5" edition = "2021" resolver = "2" diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index d916407c..22d934fc 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only" -version = "0.3.4" +version = "0.3.5" edition = "2021" resolver = "2" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index f6b5d4df..f709ecbb 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.3.4" +version = "0.3.5" edition = "2021" resolver = "2" diff --git a/resources/config/jmap.toml b/resources/config/jmap.toml index 3759bee5..54170f37 100644 --- a/resources/config/jmap.toml +++ b/resources/config/jmap.toml @@ -37,7 +37,9 @@ timeout = "30s" directory = "__DIRECTORY__" [jmap.http] -#headers = ["Access-Control-Allow-Origin: *", "Access-Control-Allow-Methods: POST, GET"] +#headers = ["Access-Control-Allow-Origin: *", +# "Access-Control-Allow-Methods: POST, GET, HEAD, OPTIONS", +# "Access-Control-Allow-Headers: *"] [jmap.encryption] enable = true