diff --git a/CHANGELOG.md b/CHANGELOG.md index db262f7..d19d57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.16.27] - 2026-10-18 + +If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. + +## Added + +## Changed + +## Fixed +- IMAP: + - `APPEND` and `COPY`/`MOVE` into another account's mailbox do not change the JMAP `EmailDelivery` state. + - `SEARCH RETURN` with `MIN` or `MAX` alongside `ALL` returns only the minimum and maximum in `ALL`, and `SAVE` combined with `MIN` or `MAX` and `ALL` or `COUNT` saves only the minimum and maximum instead of every match. +- iTIP: Merging a received update does not copy the `VTIMEZONE` definitions it carries, so an occurrence moved by an external organizer can reference a `TZID` (for example `W. Europe Standard Time`) the stored event no longer defines, and Apple Calendar shows both the original and the moved occurrence. +- OAuth: Loopback redirect URIs without a path (for example `http://127.0.0.1:45678`) are not matched against registered loopback URIs on any port and are rejected by dynamic client registration. +- WebDAV: `PROPFIND` `allprop` returns `DAV:current-user-principal`, so Windows Explorer shows every file and folder under the account name. + ## [0.16.26] - 2026-10-11 If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. @@ -11,6 +27,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - JMAP: `Mailbox/set` allows moving a mailbox under a parent that already has a child with the same name, and rejects renames that only change the case of a mailbox name with `alreadyExists`. ## Changed +- Breaking: x86_64 binaries and Docker images (except musl builds) now require a CPU with the x86-64-v2 instruction set (which includes SSE3, SSSE3, SSE4.1, SSE4.2 and POPCNT), and the server refuses to start on older CPUs. Virtual machines using a generic CPU model such as QEMU's "Common KVM processor" are affected. To fix it, set the VM CPU type to `host` or `x86-64-v2-AES` (Proxmox), or switch to the musl build: the `-alpine` Docker tags (for example `stalwartlabs/stalwart:v0.16-alpine`) or the `x86_64-unknown-linux-musl` binary. ## Fixed - DNSSEC: Use `hickory-resolver` patched fork at https://github.com/stalwartlabs/hickory-dns. diff --git a/Cargo.lock b/Cargo.lock index b534d0a..00facb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,7 +37,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ "crypto-common 0.2.2", - "inout 0.2.2", + "inout 0.2.3", ] [[package]] @@ -116,7 +116,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.8.61", + "zerocopy 0.8.63", ] [[package]] @@ -342,7 +342,7 @@ checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -375,7 +375,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -425,9 +425,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -436,9 +436,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -980,7 +980,7 @@ checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1106,9 +1106,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "jobserver", @@ -1242,7 +1242,7 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer 0.12.1", "crypto-common 0.2.2", - "inout 0.2.2", + "inout 0.2.3", ] [[package]] @@ -1284,9 +1284,9 @@ checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "futures-core", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "common" -version = "0.16.26" +version = "0.16.27" dependencies = [ "aes-gcm-siv", "ahash", @@ -1363,7 +1363,7 @@ dependencies = [ "serde", "serde_json", "sha1 0.11.0", - "sha2 0.11.0", + "sha2 0.11.1", "sieve-rs", "smtp-proto", "socket2 0.6.5", @@ -1484,7 +1484,7 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "coordinator" -version = "0.16.26" +version = "0.16.27" dependencies = [ "async-nats", "futures", @@ -1836,7 +1836,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1869,7 +1869,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core 0.24.1", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1886,7 +1886,7 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "dav" -version = "0.16.26" +version = "0.16.27" dependencies = [ "calcard", "chrono", @@ -1909,7 +1909,7 @@ dependencies = [ [[package]] name = "dav-proto" -version = "0.16.26" +version = "0.16.27" dependencies = [ "calcard", "chrono", @@ -2039,9 +2039,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" dependencies = [ "serde_core", ] @@ -2122,7 +2122,7 @@ dependencies = [ [[package]] name = "directory" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "argon2 0.6.0", @@ -2142,7 +2142,7 @@ dependencies = [ "serde", "serde_json", "sha1 0.11.0", - "sha2 0.11.0", + "sha2 0.11.1", "store", "tokio", "totp-rs", @@ -2179,7 +2179,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2193,12 +2193,12 @@ dependencies = [ [[package]] name = "dns-update" -version = "0.5.9" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55d6bab7e8f88b1ff3079da050676de0fbc0589014bb52bf39975affb8ad19" +checksum = "3bee8173806ecce181535651e457cac50ced7920d48e2da3920ded2c44bf1246" dependencies = [ "aws-lc-rs", - "base64 0.23.1", + "encodify", "hex", "hickory-net", "hickory-proto", @@ -2363,7 +2363,7 @@ dependencies = [ [[package]] name = "email" -version = "0.16.26" +version = "0.16.27" dependencies = [ "aes 0.9.3", "aes-gcm 0.11.1", @@ -2429,9 +2429,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" @@ -2481,10 +2481,10 @@ dependencies = [ [[package]] name = "event_macro" -version = "0.16.26" +version = "0.16.27" dependencies = [ "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2838,7 +2838,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3009,7 +3009,7 @@ dependencies = [ [[package]] name = "groupware" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "calcard", @@ -3290,7 +3290,7 @@ dependencies = [ [[package]] name = "http" -version = "0.16.26" +version = "0.16.27" dependencies = [ "async-stream", "base64 0.23.1", @@ -3322,7 +3322,7 @@ dependencies = [ "serde", "serde_json", "services", - "sha2 0.11.0", + "sha2 0.11.1", "smtp", "store", "tokio", @@ -3386,7 +3386,7 @@ dependencies = [ [[package]] name = "http_proto" -version = "0.16.26" +version = "0.16.27" dependencies = [ "common", "compact_str", @@ -3873,7 +3873,7 @@ checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a" [[package]] name = "imap" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "common", @@ -3898,7 +3898,7 @@ dependencies = [ [[package]] name = "imap_proto" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "base64 0.23.1", @@ -3913,7 +3913,7 @@ dependencies = [ [[package]] name = "inbuxa" -version = "0.16.26" +version = "0.16.27" dependencies = [ "common", "coordinator", @@ -3921,7 +3921,7 @@ dependencies = [ "directory", "email", "groupware", - "http 0.16.26", + "http 0.16.27", "http_proto", "imap", "jmap", @@ -3962,7 +3962,7 @@ dependencies = [ "registry", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "store", "tokio", "toml", @@ -4056,9 +4056,9 @@ dependencies = [ [[package]] name = "inout" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +checksum = "d8c0d789294fb457b791f7797a04478704788f30b407e5d0b01deaa846572d9f" dependencies = [ "block-padding 0.4.2", "hybrid-array 0.4.15", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.16.26" +version = "0.16.27" dependencies = [ "async-stream", "base64 0.23.1", @@ -4245,7 +4245,7 @@ dependencies = [ "serde_json", "services", "sha1 0.11.0", - "sha2 0.11.0", + "sha2 0.11.1", "sieve-rs", "smtp", "smtp-proto", @@ -4296,7 +4296,7 @@ dependencies = [ [[package]] name = "jmap_proto" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "calcard", @@ -4638,9 +4638,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.29" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +checksum = "f710a23e6dbf193214fd46ca56a9d6864e550abe86202184532ae7275e46de19" dependencies = [ "cc", "libc", @@ -4739,7 +4739,7 @@ version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" dependencies = [ - "sha2 0.11.0", + "sha2 0.11.1", ] [[package]] @@ -4802,7 +4802,7 @@ dependencies = [ [[package]] name = "managesieve" -version = "0.16.26" +version = "0.16.27" dependencies = [ "common", "compact_str", @@ -4937,7 +4937,7 @@ checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "migration" -version = "0.16.26" +version = "0.16.27" dependencies = [ "common", "email", @@ -5187,7 +5187,7 @@ dependencies = [ [[package]] name = "nlp" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "hashify", @@ -5725,7 +5725,7 @@ dependencies = [ "digest 0.11.3", "hmac 0.13.0", "password-hash 0.6.1", - "sha2 0.11.0", + "sha2 0.11.1", ] [[package]] @@ -5765,19 +5765,21 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa" dependencies = [ "memchr", + "psm", + "stacker", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f" dependencies = [ "pest", "pest_generator", @@ -5785,22 +5787,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "pest_meta" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1" dependencies = [ "pest", ] @@ -6019,7 +6021,7 @@ dependencies = [ [[package]] name = "pop3" -version = "0.16.26" +version = "0.16.27" dependencies = [ "common", "directory", @@ -6064,7 +6066,7 @@ dependencies = [ "md-5 0.11.0", "memchr", "rand 0.10.3", - "sha2 0.11.0", + "sha2 0.11.1", "stringprep", ] @@ -6110,7 +6112,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.61", + "zerocopy 0.8.63", ] [[package]] @@ -6189,7 +6191,7 @@ dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -6283,6 +6285,15 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "ptr_meta" version = "0.3.2" @@ -6300,7 +6311,7 @@ checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -6798,7 +6809,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -6832,7 +6843,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "registry" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "hashify", @@ -7016,7 +7027,7 @@ checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -7210,9 +7221,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log", @@ -7226,9 +7237,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -7363,9 +7374,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" dependencies = [ "dyn-clone", "either", @@ -7377,19 +7388,19 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "scim" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "base64 0.23.1", @@ -7405,7 +7416,7 @@ dependencies = [ "scim-proto", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "store", "trc", "types", @@ -7415,7 +7426,7 @@ dependencies = [ [[package]] name = "scim-proto" -version = "0.16.26" +version = "0.16.27" dependencies = [ "hashify", "serde", @@ -7438,7 +7449,7 @@ dependencies = [ "password-hash 0.6.1", "pbkdf2", "salsa20", - "sha2 0.11.0", + "sha2 0.11.1", ] [[package]] @@ -7506,9 +7517,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "sequoia-openpgp" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbc8f9818a6fad141d85993777ba298ee52c80a09bd23d45dda461a6b7c83cb" +checksum = "1460dbd9c736920a78bd450eaa6902304605c493bd2222121849ba01ab8df997" dependencies = [ "aes 0.8.4", "aes-gcm 0.10.3", @@ -7551,8 +7562,7 @@ dependencies = [ "p256", "p384", "p521", - "rand 0.9.5", - "rand_core 0.6.4", + "rand 0.10.3", "regex", "regex-syntax", "ripemd", @@ -7605,7 +7615,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -7616,15 +7626,16 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash 0.2.0", "indexmap 2.14.2", "itoa", "memchr", @@ -7652,7 +7663,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -7690,7 +7701,7 @@ dependencies = [ "indexmap 2.14.2", "jiff", "schemars 0.9.0", - "schemars 1.2.2", + "schemars 1.2.3", "serde_core", "serde_json", "serde_with_macros", @@ -7706,7 +7717,7 @@ dependencies = [ "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -7754,12 +7765,12 @@ checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "services" -version = "0.16.26" +version = "0.16.27" dependencies = [ "aes-gcm 0.11.1", "aho-corasick", @@ -7784,7 +7795,7 @@ dependencies = [ "registry", "reqwest 0.13.5", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "smtp", "smtp-proto", "spam-filter", @@ -7873,9 +7884,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.1", @@ -8068,13 +8079,13 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "smtp" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "base64 0.23.1", @@ -8098,7 +8109,7 @@ dependencies = [ "rustls-webpki", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "sieve-rs", "smtp-proto", "spam-filter", @@ -8165,7 +8176,7 @@ dependencies = [ [[package]] name = "spam-filter" -version = "0.16.26" +version = "0.16.27" dependencies = [ "common", "compact_str", @@ -8278,6 +8289,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -8286,7 +8310,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "store" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "arc-swap", @@ -8408,9 +8432,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -8445,7 +8469,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -8546,7 +8570,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "aws-lc-rs", @@ -8568,7 +8592,7 @@ dependencies = [ "form_urlencoded", "futures", "groupware", - "http 0.16.26", + "http 0.16.27", "http_proto", "hyper", "hyper-util", @@ -8601,7 +8625,7 @@ dependencies = [ "serde_json", "serial_test", "services", - "sha2 0.11.0", + "sha2 0.11.1", "sieve-rs", "smtp", "smtp-proto", @@ -8656,7 +8680,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -8771,9 +8795,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.2" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "bytes", "libc", @@ -8794,7 +8818,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -8874,9 +8898,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", @@ -8934,9 +8958,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.16+spec-1.1.0" +version = "0.25.17+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f38f3af8bf71841c67a990f4e2b52143b2d0a1a994c72513ff4c14977fc722" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" dependencies = [ "indexmap 2.14.2", "toml_datetime", @@ -8946,9 +8970,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.4+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow", ] @@ -9025,7 +9049,7 @@ dependencies = [ "rand 0.10.3", "serde", "sha1 0.11.0", - "sha2 0.11.0", + "sha2 0.11.1", "url", "zeroize", ] @@ -9161,7 +9185,7 @@ dependencies = [ [[package]] name = "trc" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "base64 0.23.1", @@ -9270,7 +9294,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "types" -version = "0.16.26" +version = "0.16.27" dependencies = [ "blake3", "compact_str", @@ -9322,9 +9346,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" @@ -9439,7 +9463,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utils" -version = "0.16.26" +version = "0.16.27" dependencies = [ "ahash", "arcstr", @@ -9470,9 +9494,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.27.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -9642,7 +9666,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -10157,7 +10181,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure 0.14.0", ] @@ -10304,7 +10328,7 @@ dependencies = [ "hashbrown 0.16.1", "keyed-set", "rand 0.8.8", - "schemars 1.2.2", + "schemars 1.2.3", "serde", "token-cell", "zenoh-result", @@ -10617,7 +10641,7 @@ dependencies = [ "libc", "libloading", "pnet_datalink", - "schemars 1.2.2", + "schemars 1.2.3", "serde", "serde_json", "shellexpand", @@ -10641,11 +10665,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.61" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ - "zerocopy-derive 0.8.61", + "zerocopy-derive 0.8.63", ] [[package]] @@ -10661,9 +10685,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.61" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", @@ -10687,7 +10711,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure 0.14.0", ] @@ -10743,7 +10767,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -10787,9 +10811,9 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" dependencies = [ "bumpalo", "crc32fast", @@ -10817,9 +10841,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.1.0+zstd.1.5.7" +version = "2.1.1+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078" dependencies = [ "cc", "pkg-config", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 49526bd..e411eb2 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common" -version = "0.16.26" +version = "0.16.27" edition = "2024" build = "build.rs" @@ -20,7 +20,7 @@ mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "1.0" } mail-auth = { version = "0.13", features = ["generate", "arc"] } smtp-proto = { version = "0.2.3", features = ["rkyv"] } -dns-update = { version = "0.5" } +dns-update = { version = "0.6" } calcard = { version = "0.3", features = ["rkyv"] } ahash = { version = "0.8.12", features = ["serde"] } parking_lot = "0.12.5" diff --git a/crates/common/src/auth/oauth/registration.rs b/crates/common/src/auth/oauth/registration.rs index 56d7711..9cd11d6 100644 --- a/crates/common/src/auth/oauth/registration.rs +++ b/crates/common/src/auth/oauth/registration.rs @@ -212,15 +212,23 @@ pub fn loopback_redirect_parts(uri: &str) -> Option<(&str, &str)> { for host in ["127.0.0.1", "[::1]"] { if let Some(rest) = uri.strip_prefix(host) { - if let Some(path) = rest.strip_prefix('/') { - return Some((host, path)); - } else if let Some(after_colon) = rest.strip_prefix(':') - && let Some((port, path)) = after_colon.split_once('/') - && !port.is_empty() - && port.bytes().all(|b| b.is_ascii_digit()) - { - return Some((host, path)); - } + let rest = match rest.strip_prefix(':') { + Some(after_colon) => { + let rest = after_colon.trim_start_matches(|c: char| c.is_ascii_digit()); + if rest.len() == after_colon.len() { + return None; + } + rest + } + None => rest, + }; + return if let Some(path) = rest.strip_prefix('/') { + Some((host, path)) + } else if rest.is_empty() || rest.starts_with('?') { + Some((host, rest)) + } else { + None + }; } } None @@ -293,3 +301,55 @@ pub fn validate_grant_metadata( Ok(()) } + +#[cfg(test)] +mod tests { + use super::{loopback_redirect_parts, redirect_uri_matches}; + + #[test] + fn loopback_matches_any_port() { + // Loopback redirects match regardless of port and of an empty versus "/" path (RFC 8252 §7.3) + for (registered, presented) in [ + ("http://127.0.0.1", "http://127.0.0.1:45678"), + ("http://127.0.0.1/", "http://127.0.0.1:45678"), + ("http://127.0.0.1", "http://127.0.0.1:45678/"), + ("http://127.0.0.1/cb", "http://127.0.0.1:45678/cb"), + ("http://127.0.0.1/?a=1", "http://127.0.0.1:45678?a=1"), + ("http://[::1]", "http://[::1]:8080"), + ("http://[::1]/", "http://[::1]:8080/"), + ] { + assert!( + redirect_uri_matches(registered, presented), + "{registered} should match {presented}" + ); + } + + // Different paths, hosts or schemes never match + for (registered, presented) in [ + ("http://127.0.0.1/cb", "http://127.0.0.1:45678"), + ("http://127.0.0.1", "http://127.0.0.1:45678/cb"), + ("http://127.0.0.1", "http://[::1]:45678"), + ("http://127.0.0.1", "https://127.0.0.1:45678"), + ("http://127.0.0.1", "http://127.0.0.1.evil.com"), + ("http://127.0.0.1", "http://127.0.0.1:80@evil.com"), + ("http://127.0.0.1", "http://127.0.0.1:45678#frag"), + ] { + assert!( + !redirect_uri_matches(registered, presented), + "{registered} should not match {presented}" + ); + } + + // Malformed loopback URIs are not recognised + for uri in [ + "http://127.0.0.1:", + "http://127.0.0.1:/cb", + "http://127.0.0.1:abc/", + "http://127.0.0.1.evil.com/", + "http://127.0.0.1@evil.com/", + "http://localhost:8080/", + ] { + assert_eq!(loopback_redirect_parts(uri), None, "{uri}"); + } + } +} diff --git a/crates/common/src/manager/application.rs b/crates/common/src/manager/application.rs index d2c529e..1548703 100644 --- a/crates/common/src/manager/application.rs +++ b/crates/common/src/manager/application.rs @@ -6,13 +6,19 @@ * Modified by Coffey Labs in 2026 for INBUXA. */ -use crate::{Server, manager::fetch_resource}; +use crate::{ + Server, + manager::{ + defaults::{WEBUI_LATEST_URL, WEBUI_MAX_MAJOR_VERSION, WEBUI_PINNED_URL}, + fetch_resource, + }, +}; use ahash::AHashMap; use arc_swap::ArcSwap; use registry::schema::{enums::CompressionAlgo, structs::Application}; use std::{ borrow::Cow, - io::{self, Cursor, Read}, + io::{self, Cursor, Read, Seek}, path::{Path, PathBuf}, sync::{ Arc, @@ -199,7 +205,11 @@ impl WebApplicationManager { Self { base_path, blob_key: BlobHash::generate(format!("{}{}", APP_BLOB_PREFIX, app.id.id()).as_bytes()), - url: app.object.resource_url, + url: if app.object.resource_url == WEBUI_LATEST_URL { + WEBUI_PINNED_URL.to_string() + } else { + app.object.resource_url + }, description: app.object.description, expiry: app.object.auto_update_frequency.as_secs(), oauth_client_id: app.object.oauth_client_id, @@ -263,6 +273,15 @@ impl WebApplicationManager { .ctx(Key::Url, url.clone()) .details("Failed to decompress application bundle") })?; + if let Some(version) = unsupported_bundle_version(&mut archive) { + return Err(trc::ResourceEvent::Error + .caused_by(trc::location!()) + .ctx(Key::Url, url.clone()) + .details(format!( + "Application bundle version {version} is for a newer server, set the resource URL to {WEBUI_PINNED_URL}" + ))); + } + let mut resources = AHashMap::with_capacity(archive.len()); for i in 0..archive.len() { let mut file = archive.by_index(i).map_err(|err| { @@ -498,6 +517,19 @@ fn rewrite_index(html: &str, prefix: &str, oauth_client_id_meta: Option<&str>) - out.into_bytes() } +fn unsupported_bundle_version(archive: &mut zip::ZipArchive) -> Option { + let file = archive.by_name("VERSION").ok()?; + let mut version = String::new(); + let is_supported = file.take(32).read_to_string(&mut version).is_ok() + && version + .trim() + .split('.') + .next() + .and_then(|major| major.parse::().ok()) + .is_some_and(|major| major <= WEBUI_MAX_MAJOR_VERSION); + (!is_supported).then(|| version.trim().to_string()) +} + fn oauth_client_id_meta(client_id: &str) -> String { let mut meta = String::with_capacity(OAUTH_CLIENT_ID.len() + client_id.len()); meta.push_str(") -> zip::ZipArchive>> { + let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new())); + let options = zip::write::SimpleFileOptions::default(); + writer.start_file("index.html", options).unwrap(); + io::Write::write_all(&mut writer, INDEX.as_bytes()).unwrap(); + if let Some(version) = version { + writer.start_file("VERSION", options).unwrap(); + io::Write::write_all(&mut writer, version.as_bytes()).unwrap(); + } + zip::ZipArchive::new(writer.finish().unwrap()).unwrap() + } + + #[test] + fn bundles_newer_than_the_supported_major_version_are_rejected() { + // Bundles without VERSION or with a 1.x VERSION are accepted, anything else is rejected + assert_eq!(unsupported_bundle_version(&mut bundle(None)), None); + assert_eq!( + unsupported_bundle_version(&mut bundle(Some("1.0.12\n"))), + None + ); + assert_eq!( + unsupported_bundle_version(&mut bundle(Some("2.0.0\n"))), + Some("2.0.0".to_string()) + ); + assert_eq!( + unsupported_bundle_version(&mut bundle(Some("10.1.0"))), + Some("10.1.0".to_string()) + ); + assert_eq!( + unsupported_bundle_version(&mut bundle(Some("garbage"))), + Some("garbage".to_string()) + ); + } } diff --git a/crates/common/src/manager/defaults.rs b/crates/common/src/manager/defaults.rs index 2203dde..76686ce 100644 --- a/crates/common/src/manager/defaults.rs +++ b/crates/common/src/manager/defaults.rs @@ -28,6 +28,12 @@ use store::{ }, }; +pub const WEBUI_LATEST_URL: &str = + "https://github.com/stalwartlabs/webui/releases/latest/download/webui.zip"; +pub const WEBUI_PINNED_URL: &str = + "https://github.com/stalwartlabs/webui/releases/download/v1.0.11/webui.zip"; +pub const WEBUI_MAX_MAJOR_VERSION: u32 = 1; + pub const ASN_IPV4: &str = "https://github.com/sapics/ip-location-db/releases/download/latest/origin-asn-ipv4.csv"; pub const ASN_IPV6: &str = diff --git a/crates/coordinator/Cargo.toml b/crates/coordinator/Cargo.toml index e5f9bdc..40bb9ca 100644 --- a/crates/coordinator/Cargo.toml +++ b/crates/coordinator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coordinator" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index fc38ace..d976289 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav-proto" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index d3d3a1a..28b20bf 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -366,7 +366,7 @@ impl Rfc1123DateTime { } impl DavProperty { - pub const ALL_PROPS: [DavProperty; 11] = [ + pub const ALL_PROPS: [DavProperty; 10] = [ DavProperty::WebDav(WebDavProperty::CreationDate), DavProperty::WebDav(WebDavProperty::DisplayName), DavProperty::WebDav(WebDavProperty::GetETag), @@ -374,7 +374,6 @@ impl DavProperty { DavProperty::WebDav(WebDavProperty::ResourceType), DavProperty::WebDav(WebDavProperty::LockDiscovery), DavProperty::WebDav(WebDavProperty::SupportedLock), - DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), DavProperty::WebDav(WebDavProperty::GetContentLanguage), DavProperty::WebDav(WebDavProperty::GetContentLength), DavProperty::WebDav(WebDavProperty::GetContentType), @@ -390,7 +389,6 @@ impl DavProperty { | DavProperty::WebDav(WebDavProperty::ResourceType) | DavProperty::WebDav(WebDavProperty::LockDiscovery) | DavProperty::WebDav(WebDavProperty::SupportedLock) - | DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) | DavProperty::WebDav(WebDavProperty::GetContentLanguage) | DavProperty::WebDav(WebDavProperty::GetContentLength) | DavProperty::WebDav(WebDavProperty::GetContentType) diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 0d4943d..527f3aa 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index a245c0e..79dad93 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -57,7 +57,7 @@ use groupware::{ use http_proto::HttpResponse; use hyper::StatusCode; use registry::schema::{enums::Permission, prelude::ObjectType}; -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use store::{ ValueKey, registry::RegistryQuery, @@ -1725,13 +1725,20 @@ async fn add_base_collection_response( )); return Ok(()); } - PropFind::AllProp(_) => [ - DavProperty::WebDav(WebDavProperty::ResourceType), - DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), - DavProperty::WebDav(WebDavProperty::SupportedReportSet), - ] - .as_slice(), - PropFind::Prop(items) => items, + PropFind::AllProp(include) => { + let mut props = Vec::with_capacity(include.len() + 2); + props.extend([ + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + ]); + for prop in include { + if !props.contains(prop) { + props.push(prop.clone()); + } + } + Cow::Owned(props) + } + PropFind::Prop(items) => Cow::Borrowed(items.as_slice()), }; let mut fields = Vec::with_capacity(properties.len()); @@ -1741,9 +1748,9 @@ async fn add_base_collection_response( .await .caused_by(trc::location!())?; - for prop in properties { + for prop in properties.iter() { response.set_namespace(prop.namespace()); - match &prop { + match prop { DavProperty::WebDav(WebDavProperty::ResourceType) => { fields.push(DavPropertyValue::new( prop.clone(), diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index e21054d..ba43d1f 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -448,17 +448,23 @@ fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec) { +pub fn itip_merge_changes(ical: &mut ICalendar, itip: &ICalendar, changes: Vec) { let mut remove_component_ids: Vec = Vec::new(); for action in changes { match action { @@ -600,6 +600,85 @@ pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec) { if !remove_component_ids.is_empty() { ical.remove_component_ids(&remove_component_ids); } + + merge_timezones(ical, itip); +} + +fn merge_timezones(ical: &mut ICalendar, itip: &ICalendar) { + let Some(root) = ical + .components + .first() + .filter(|root| root.component_type == ICalendarComponentType::VCalendar) + else { + return; + }; + let mut insert_at = root + .component_ids + .iter() + .position(|component_id| { + ical.component_by_id(*component_id) + .is_none_or(|comp| comp.component_type != ICalendarComponentType::VTimezone) + }) + .unwrap_or(root.component_ids.len()); + + for tz in itip.timezones() { + let Some(tz_id) = timezone_id(tz) else { + continue; + }; + + let mut is_referenced = false; + for component in &ical.components { + if component.component_type == ICalendarComponentType::VTimezone { + if timezone_id(component) == Some(tz_id) { + is_referenced = false; + break; + } + } else if !is_referenced { + is_referenced = component + .entries + .iter() + .any(|entry| entry.tz_id() == Some(tz_id)); + } + } + if !is_referenced { + continue; + } + + let tz_component_id = ical.components.len() as u32; + ical.components.reserve(tz.component_ids.len() + 1); + ical.components.push(ICalendarComponent { + component_type: ICalendarComponentType::VTimezone, + entries: tz.entries.clone(), + component_ids: Vec::new(), + }); + ical.components.extend( + tz.component_ids + .iter() + .filter_map(|component_id| itip.component_by_id(*component_id)) + .map(|item| ICalendarComponent { + component_type: item.component_type.clone(), + entries: item.entries.clone(), + component_ids: Vec::new(), + }), + ); + let last_item_id = ical.components.len() as u32; + if let Some(tz_component) = ical.components.get_mut(tz_component_id as usize) { + tz_component.component_ids = (tz_component_id + 1..last_item_id).collect(); + } + if let Some(root) = ical.components.first_mut() { + root.component_ids.insert(insert_at, tz_component_id); + insert_at += 1; + } + } + + ical.add_missing_timezones(); +} + +fn timezone_id(component: &ICalendarComponent) -> Option<&str> { + component + .property(&ICalendarProperty::Tzid) + .and_then(|entry| entry.values.first()) + .and_then(|value| value.as_text()) } pub fn itip_method(ical: &ICalendar) -> Result<&ICalendarMethod, ItipError> { diff --git a/crates/http-proto/Cargo.toml b/crates/http-proto/Cargo.toml index 847e376..87af79a 100644 --- a/crates/http-proto/Cargo.toml +++ b/crates/http-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_proto" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index d98f5d1..32572d1 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/imap-proto/Cargo.toml b/crates/imap-proto/Cargo.toml index 7be3049..4f3678d 100644 --- a/crates/imap-proto/Cargo.toml +++ b/crates/imap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap_proto" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 46e3ab2..f7a3681 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index beadb15..f85645c 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -169,6 +169,7 @@ impl SessionData { .broadcast_push_notification(PushNotification::StateChange( StateChange::new(account_id) .with_change_id(change_id) + .with_change(DataType::EmailDelivery) .with_change(DataType::Email) .with_change(DataType::Mailbox) .with_change(DataType::Thread), diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs index 6a13bff..f0eeabe 100644 --- a/crates/imap/src/op/copy_move.rs +++ b/crates/imap/src/op/copy_move.rs @@ -648,6 +648,7 @@ impl SessionData { .broadcast_push_notification(PushNotification::StateChange( StateChange::new(dest_account_id) .with_change_id(change_id) + .with_change(DataType::EmailDelivery) .with_change(DataType::Email) .with_change(DataType::Thread) .with_change(DataType::Mailbox), diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index 4139f88..aa09b87 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -175,9 +175,14 @@ impl SessionData { None }; let mut imap_ids = Vec::with_capacity(results_len); + let return_all = arguments.result_options.is_empty() + || arguments.result_options.contains(&ResultOption::All); mailbox.map_search_results( result_set.into_iter(), is_uid, + return_all + || (saved_results.is_some() + && arguments.result_options.contains(&ResultOption::Count)), arguments.result_options.contains(&ResultOption::Min), arguments.result_options.contains(&ResultOption::Max), &mut min, @@ -247,13 +252,7 @@ impl SessionData { } else { None }, - ids: if arguments.result_options.is_empty() - || arguments.result_options.contains(&ResultOption::All) - { - imap_ids - } else { - vec![] - }, + ids: if return_all { imap_ids } else { vec![] }, is_sort, is_esearch: arguments.is_esearch, highest_modseq, @@ -747,6 +746,7 @@ impl SelectedMailbox { &self, ids: impl Iterator, is_uid: bool, + collect_all: bool, find_min: bool, find_max: bool, min: &mut Option<(u32, ImapId)>, @@ -759,26 +759,25 @@ impl SelectedMailbox { let find_min_or_max = find_min || find_max; for document_id in ids { if let Some((id, imap_id)) = state.map_result_id(document_id, is_uid) { - if find_min_or_max { - if find_min { - if let Some((prev_min, _)) = min { - if id < *prev_min { - *min = Some((id, imap_id)); - } - } else { + if find_min { + if let Some((prev_min, _)) = min { + if id < *prev_min { *min = Some((id, imap_id)); } + } else { + *min = Some((id, imap_id)); } - if find_max { - if let Some((prev_max, _)) = max { - if id > *prev_max { - *max = Some((id, imap_id)); - } - } else { + } + if find_max { + if let Some((prev_max, _)) = max { + if id > *prev_max { *max = Some((id, imap_id)); } + } else { + *max = Some((id, imap_id)); } - } else { + } + if collect_all || !find_min_or_max { imap_ids.push(id); if let Some(r) = saved_results.as_mut() { r.push(imap_id) @@ -787,11 +786,12 @@ impl SelectedMailbox { *total += 1; } } - if find_min || find_max { - for (id, imap_id) in [min, max].into_iter().flatten() { - imap_ids.push(*id); + if find_min_or_max && !collect_all { + let max = max.filter(|(max_id, _)| min.is_none_or(|(min_id, _)| min_id != *max_id)); + for (id, imap_id) in [*min, max].into_iter().flatten() { + imap_ids.push(id); if let Some(r) = saved_results.as_mut() { - r.push(*imap_id) + r.push(imap_id) } } } diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index 3d57f31..f5ba95b 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap_proto" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 7ed0169..d565ffa 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 2db0128..ca4b07d 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -10,7 +10,7 @@ use crate::{ changes::state::JmapCacheState, email::{PatchResult, handle_email_patch, ingested_into_object}, }; -use common::{Server, auth::AccessToken}; +use common::{Server, auth::AccessToken, ipc::PushNotification}; use email::{ cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess}, mailbox::JUNK_ID, @@ -37,7 +37,10 @@ use jmap_tools::{Key, Value}; use std::future::Future; use store::write::BatchBuilder; use trc::AddContext; -use types::acl::Acl; +use types::{ + acl::Acl, + type_state::{DataType, StateChange}, +}; use utils::map::vec_map::VecMap; pub trait JmapEmailCopy: Sync + Send { @@ -96,6 +99,7 @@ impl JmapEmailCopy for Server { let mut destroy_ids = Vec::new(); let mut train_batch = BatchBuilder::new(); let mut did_train = false; + let mut last_change_id = None; train_batch.with_account_id(from_account_id); 'create: for (id, create) in request.create.into_valid() { @@ -232,6 +236,7 @@ impl JmapEmailCopy for Server { .await? { Ok(email) => { + last_change_id = Some(email.change_id); if train_spam { self.add_account_spam_sample( &mut train_batch, @@ -277,7 +282,16 @@ impl JmapEmailCopy for Server { } // Update state - if !response.created.is_empty() { + if let Some(change_id) = last_change_id { + self.broadcast_push_notification(PushNotification::StateChange( + StateChange::new(account_id) + .with_change_id(change_id) + .with_change(DataType::EmailDelivery) + .with_change(DataType::Email) + .with_change(DataType::Mailbox) + .with_change(DataType::Thread), + )) + .await; response.new_state = self.get_cached_messages(account_id).await?.get_state(false); } diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index d2b3d18..3a6a41d 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -223,6 +223,7 @@ impl EmailImport for Server { self.broadcast_push_notification(PushNotification::StateChange( StateChange::new(account_id) .with_change_id(change_id) + .with_change(DataType::EmailDelivery) .with_change(DataType::Email) .with_change(DataType::Mailbox) .with_change(DataType::Thread), diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 49eb88d..2af3df2 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -1233,12 +1233,21 @@ impl EmailSet for Server { if let Some(change_id) = last_change_id { if response.updated.is_empty() && response.destroyed.is_empty() { // Message ingest does not broadcast state changes + let mut state_change = StateChange::new(account_id) + .with_change_id(change_id) + .with_change(DataType::Email) + .with_change(DataType::Mailbox) + .with_change(DataType::Thread); + if !response.created.is_empty() { + state_change.set_change(DataType::EmailDelivery); + } + self.broadcast_push_notification(PushNotification::StateChange(state_change)) + .await; + } else if !response.created.is_empty() { self.broadcast_push_notification(PushNotification::StateChange( StateChange::new(account_id) .with_change_id(change_id) - .with_change(DataType::Email) - .with_change(DataType::Mailbox) - .with_change(DataType::Thread), + .with_change(DataType::EmailDelivery), )) .await; } diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index a9cbc9f..ee56dcf 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"] categories = ["email"] # Upstream offers AGPL-3.0-only OR LicenseRef-SEL; inbuxa takes the AGPL only. license = "AGPL-3.0-only" -version = "0.16.26" +version = "0.16.27" edition = "2024" [[bin]] diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index 90eafdf..817477e 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managesieve" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index db65814..30b3795 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migration" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml index 269ce4a..63a6f9e 100644 --- a/crates/nlp/Cargo.toml +++ b/crates/nlp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nlp" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index 59e41d8..cbdfe25 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pop3" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index fbecd43..a34fe4d 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "registry" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/registry/src/schema/prelude.rs b/crates/registry/src/schema/prelude.rs index 1567196..ea183b5 100644 --- a/crates/registry/src/schema/prelude.rs +++ b/crates/registry/src/schema/prelude.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ pub use crate::jmap::IntoValue; pub use crate::jmap::JmapValue; @@ -31,6 +33,7 @@ pub use crate::types::string::StringValidator; pub use serde::{Deserialize, Serialize}; pub use std::borrow::Cow; pub use std::str::FromStr; +use std::sync::LazyLock; pub use types::blob::BlobId; pub use types::id::Id; pub use utils::map::vec_map::VecMap; @@ -56,3 +59,17 @@ pub const OBJ_FILTER_ACCOUNT: u64 = 1 << 2; pub const OBJ_FILTER_TENANT: u64 = 1 << 3; pub const MASKED_PASSWORD: &str = "****"; + +// Temporary migration aid, will be removed by v1. +pub static INCLUDE_SECRETS: LazyLock = LazyLock::new(|| { + std::env::var("INBUXA_INCLUDE_SECRETS") + .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true")) +}); + +pub fn secret_value(value: impl IntoValue) -> JmapValue<'static> { + if *INCLUDE_SECRETS { + value.into_value() + } else { + JmapValue::Str(MASKED_PASSWORD.into()) + } +} diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 529a9ec..96704b7 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -169,13 +169,10 @@ impl IntoValue for AccountPassword { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(5); if self.secret.is_some() { - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); } if self.current_secret.is_some() { - map.insert_unchecked( - Property::CurrentSecret, - JmapValue::Str(MASKED_PASSWORD.into()), - ); + map.insert_unchecked(Property::CurrentSecret, secret_value(self.current_secret)); } map.insert_unchecked(Property::OtpAuth, self.otp_auth.into_value()); JmapValue::Object(map) @@ -399,7 +396,7 @@ impl IntoValue for AcmeProvider { map.insert_unchecked(Property::ChallengeType, self.challenge_type.into_value()); map.insert_unchecked(Property::Contact, self.contact.into_value()); map.insert_unchecked(Property::Directory, self.directory.into_value()); - map.insert_unchecked(Property::AccountKey, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::AccountKey, secret_value(self.account_key)); map.insert_unchecked(Property::AccountUri, self.account_uri.into_value()); map.insert_unchecked(Property::RenewBefore, self.renew_before.into_value()); map.insert_unchecked(Property::MaxRetries, self.max_retries.into_value()); @@ -1525,7 +1522,7 @@ impl IntoValue for ApiKey { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(8); map.insert_unchecked(Property::Description, self.description.into_value()); - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); map.insert_unchecked(Property::CreatedAt, self.created_at.into_value()); map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value()); map.insert_unchecked(Property::Permissions, self.permissions.into_value()); @@ -1633,7 +1630,7 @@ impl IntoValue for AppPassword { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(8); map.insert_unchecked(Property::Description, self.description.into_value()); - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); map.insert_unchecked(Property::CreatedAt, self.created_at.into_value()); map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value()); map.insert_unchecked(Property::Permissions, self.permissions.into_value()); @@ -30554,7 +30551,7 @@ impl IntoValue for OAuthClient { map.insert_unchecked(Property::Description, self.description.into_value()); map.insert_unchecked(Property::Contacts, self.contacts.into_value()); if self.secret.is_some() { - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); } map.insert_unchecked(Property::CreatedAt, self.created_at.into_value()); map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value()); @@ -30956,10 +30953,10 @@ impl IntoValue for OtpAuth { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(4); if self.otp_code.is_some() { - map.insert_unchecked(Property::OtpCode, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::OtpCode, secret_value(self.otp_code)); } if self.otp_url.is_some() { - map.insert_unchecked(Property::OtpUrl, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::OtpUrl, secret_value(self.otp_url)); } JmapValue::Object(map) } @@ -31050,9 +31047,9 @@ impl IntoValue for PasswordCredential { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(7); map.insert_unchecked(Property::CredentialId, self.credential_id.into_value()); - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); if self.otp_auth.is_some() { - map.insert_unchecked(Property::OtpAuth, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::OtpAuth, secret_value(self.otp_auth)); } map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value()); map.insert_unchecked(Property::AllowedIps, self.allowed_ips.into_value()); @@ -34815,7 +34812,7 @@ impl IntoValue for SecondaryCredential { let mut map = jmap_tools::Map::with_capacity(9); map.insert_unchecked(Property::CredentialId, self.credential_id.into_value()); map.insert_unchecked(Property::Description, self.description.into_value()); - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); map.insert_unchecked(Property::CreatedAt, self.created_at.into_value()); map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value()); map.insert_unchecked(Property::Permissions, self.permissions.into_value()); @@ -35218,7 +35215,7 @@ impl Default for SecretKeyValue { impl IntoValue for SecretKeyValue { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(3); - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); JmapValue::Object(map) } } @@ -35503,7 +35500,7 @@ impl Default for SecretTextValue { impl IntoValue for SecretTextValue { fn into_value(self) -> JmapValue<'static> { let mut map = jmap_tools::Map::with_capacity(3); - map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into())); + map.insert_unchecked(Property::Secret, secret_value(self.secret)); JmapValue::Object(map) } } diff --git a/crates/scim-proto/Cargo.toml b/crates/scim-proto/Cargo.toml index b9fb371..cbe450d 100644 --- a/crates/scim-proto/Cargo.toml +++ b/crates/scim-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scim-proto" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/scim/Cargo.toml b/crates/scim/Cargo.toml index e9e727e..17bd549 100644 --- a/crates/scim/Cargo.toml +++ b/crates/scim/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scim" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index f955d51..a87eb9a 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "services" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] @@ -32,7 +32,7 @@ hkdf = "0.13" sha2 = "0.11" reqwest = { version = "0.13", default-features = false, features = ["rustls", "http2"]} base64 = "0.23" -dns-update = { version = "0.5" } +dns-update = { version = "0.6" } psl = "2" # inbuxa: the deliverability check mail-auth = { version = "0.13" } diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index c9f8066..30a4cfd 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -6,7 +6,7 @@ homepage = "https://inbuxa.org" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index 980a1a5..46fd629 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spam-filter" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index c20dc95..6ec01d9 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "store" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index 5049902..1bb3b83 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trc" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/trc/event-macro/Cargo.toml b/crates/trc/event-macro/Cargo.toml index 9d62c9d..c3b0c73 100644 --- a/crates/trc/event-macro/Cargo.toml +++ b/crates/trc/event-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "event_macro" -version = "0.16.26" +version = "0.16.27" edition = "2024" [lib] diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 5fb5320..4921f70 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "types" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 09c235f..7b8afc5 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.16.26" +version = "0.16.27" edition = "2024" [dependencies] diff --git a/crates/utils/proc-macros/Cargo.toml b/crates/utils/proc-macros/Cargo.toml index 3391f73..3d447c4 100644 --- a/crates/utils/proc-macros/Cargo.toml +++ b/crates/utils/proc-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proc_macros" -version = "0.16.26" +version = "0.16.27" edition = "2024" [lib] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 80f0e52..75ace81 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.16.26" +version = "0.16.27" edition = "2024" [features] @@ -82,7 +82,7 @@ form_urlencoded = "1.2.2" rkyv = { version = "0.8.18", features = ["little_endian"] } quick-xml = "0.41" jmap-tools = { version = "0.1" } -dns-update = { version = "0.5", features = ["test_provider"] } +dns-update = { version = "0.6", features = ["test_provider"] } x509-parser = "0.18" rcgen = "0.14" sha2 = "0.11" diff --git a/tests/src/imap/search.rs b/tests/src/imap/search.rs index 7aa7e75..b4ec372 100644 --- a/tests/src/imap/search.rs +++ b/tests/src/imap/search.rs @@ -29,7 +29,7 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, te imap.send("SEARCH RETURN (MIN MAX COUNT ALL) ALL").await; imap.assert_read(Type::Tagged, ResponseType::Ok) .await - .assert_contains("COUNT 10 MIN 1 MAX 10 ALL 1,10"); + .assert_contains("COUNT 10 MIN 1 MAX 10 ALL 1:10"); imap_check.send("UID SEARCH ALL").await; imap_check .assert_read(Type::Tagged, ResponseType::Ok) @@ -118,6 +118,65 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, te .await .assert_contains("MIN 2 MAX 9"); + // Saved search with MIN/MAX and ALL saves every match + imap_check.send( + "UID SEARCH RETURN (SAVE MIN MAX ALL) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore", + ) + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MIN 1 MAX 10 ALL 1,3:4,6,8,10"); + imap_check.send("UID SEARCH $").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 1 3 4 6 8 10"); + + // Saved search with MIN and COUNT saves every match + imap_check.send( + "UID SEARCH RETURN (SAVE MIN COUNT) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore", + ) + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("COUNT 6 MIN 1"); + imap_check.send("UID SEARCH $").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 1 3 4 6 8 10"); + + // Saved search with only MIN and MAX saves the extremes + imap_check.send( + "UID SEARCH RETURN (SAVE MIN MAX) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore", + ) + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MIN 1 MAX 10"); + imap_check.send("UID SEARCH $").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 1 10"); + + // Saved search with MIN and MAX on a single match saves it once + imap_check + .send("UID SEARCH RETURN (SAVE MIN MAX) UID 8") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MIN 8 MAX 8"); + imap_check.send("UID SEARCH $").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 8"); + // Sort imap_check .send("UID SORT (REVERSE SUBJECT REVERSE DATE) UTF-8 FROM Nathaniel") diff --git a/tests/src/jmap/core/event_source.rs b/tests/src/jmap/core/event_source.rs index 6b02bd3..17856a6 100644 --- a/tests/src/jmap/core/event_source.rs +++ b/tests/src/jmap/core/event_source.rs @@ -97,6 +97,37 @@ pub async fn test(test: &TestServer) { .await; assert_ping(&mut event_rx).await; + // Import email and expect EmailDelivery state change + client + .email_import( + concat!( + "From: bill@example.com\r\n", + "To: jdoe@example.com\r\n", + "Subject: TPS Report (imported)\r\n", + "\r\n", + "Did you get the memo?" + ) + .as_bytes() + .to_vec(), + [Id::from(INBOX_ID).to_string()], + None::>, + None, + ) + .await + .unwrap(); + assert_state( + &mut event_rx, + account.id_string(), + &[ + DataType::EmailDelivery, + DataType::Email, + DataType::Thread, + DataType::Mailbox, + ], + ) + .await; + assert_ping(&mut event_rx).await; + // Destroy mailbox client.mailbox_destroy(&mailbox_id, true).await.unwrap(); assert_state(&mut event_rx, account.id_string(), &[DataType::Mailbox]).await; diff --git a/tests/src/system/oidc.rs b/tests/src/system/oidc.rs index 2ba7e7c..664f44b 100644 --- a/tests/src/system/oidc.rs +++ b/tests/src/system/oidc.rs @@ -250,6 +250,9 @@ pub async fn test(test: &mut TestServer) { "http://127.0.0.1/cb", "http://127.0.0.1:54321/cb", "http://[::1]:8080/cb", + "http://127.0.0.1", + "http://127.0.0.1:54321", + "http://[::1]:8080", ] { let (status, body) = post_json_raw( &metadata.registration_endpoint, diff --git a/tests/src/webdav/cal_itip.rs b/tests/src/webdav/cal_itip.rs index b0671f5..d38d138 100644 --- a/tests/src/webdav/cal_itip.rs +++ b/tests/src/webdav/cal_itip.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use ahash::AHashMap; @@ -277,7 +279,7 @@ pub fn test() { ) { Ok(result) => match result { MergeResult::Actions(changes) => { - itip_merge_changes(ical, changes); + itip_merge_changes(ical, &message.message, changes); Ok(None) } MergeResult::Message(message) => { diff --git a/tests/src/webdav/prop.rs b/tests/src/webdav/prop.rs index a536634..637cfb1 100644 --- a/tests/src/webdav/prop.rs +++ b/tests/src/webdav/prop.rs @@ -170,6 +170,66 @@ pub async fn test(test: &TestServer, assisted_discovery: bool) { .with_status(StatusCode::MULTI_STATUS) .with_hrefs([test_path.as_str()]); + // Allprop must not return href-valued properties outside RFC 4918 unless included + let user_base_href = format!("{user_base_path}/"); + let href_props = [ + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + ]; + for (path, hrefs) in [ + ( + resource_type.base_path(), + [resource_type.collection_path(), user_base_href.as_str()], + ), + ( + user_base_path.as_str(), + [user_base_href.as_str(), test_base_path.as_str()], + ), + ( + test_base_path.as_str(), + [test_base_path.as_str(), test_path.as_str()], + ), + ] { + let response = client + .request_with_headers("PROPFIND", path, [("depth", "1")], "") + .await + .with_status(StatusCode::MULTI_STATUS) + .into_propfind_response(None); + for href in hrefs { + let properties = response.properties(href); + properties.is_defined(DavProperty::WebDav(WebDavProperty::ResourceType)); + for prop in &href_props { + properties.is_undefined(prop); + } + } + + let response = client + .request_with_headers( + "PROPFIND", + path, + [("depth", "1")], + concat!( + r#""#, + r#""#, + r#""# + ), + ) + .await + .with_status(StatusCode::MULTI_STATUS) + .into_propfind_response(None); + for href in hrefs { + response + .properties(href) + .get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)) + .with_values([format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str()]); + } + } + // Test 8 PROPFIND with prefer return=minimal let response = client .propfind_with_headers(&test_base_path, ALL_DAV_PROPERTIES, []) diff --git a/tools/fork/name-allowlist.txt b/tools/fork/name-allowlist.txt index 5ebd7d0..6f108a4 100644 --- a/tools/fork/name-allowlist.txt +++ b/tools/fork/name-allowlist.txt @@ -26,3 +26,10 @@ crates/types/src/branding.rs "STALWART" # Upstream's old default rules source, read only to treat it as unset: the # server uses the rules bundled with it (resources/spam-filter/). crates/common/src/manager/spam_rules.rs "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz" + +# Upstream's web interface downloads, used only by an install upgraded from +# Stalwart that still has its web application: inbuxa installs none +# (docs/spec/SPEC.md §5.3), and the pin keeps an upgraded one on the 1.x +# bundles this server line can serve. +crates/common/src/manager/defaults.rs "https://github.com/stalwartlabs/webui/releases/download/v1.0.11/webui.zip" +crates/common/src/manager/defaults.rs "https://github.com/stalwartlabs/webui/releases/latest/download/webui.zip" diff --git a/tools/fork/renames.py b/tools/fork/renames.py index 4f99283..d615700 100644 --- a/tools/fork/renames.py +++ b/tools/fork/renames.py @@ -41,6 +41,7 @@ TEXT_RENAMES = [ ('(vnd.stalwart)', '(vnd.inbuxa)', None), ('stalwart-webui', 'inbuxa-webui', None), ('STALWART_SPAM_', 'INBUXA_SPAM_', None), + ('STALWART_INCLUDE_SECRETS', 'INBUXA_INCLUDE_SECRETS', ('crates',)), # Configuration defaults, as upstream's generated registry code spells # them. Server code only: the tests use the same spelling for fixtures # that must match their containers and identity provider (database users,