diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9cc23c6..300f23a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,39 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
+## [0.16.24] - 2026-09-27
+
+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
+- DNS: PowerDNS Authoritative provider for automatic DNS record management.
+
+## Changed
+
+## Fixed
+- Troubleshoot tool: `TLSA` records are looked up for every MX host, including hosts whose zone is not DNSSEC signed.
+- Spam filter:
+ - OpenPhish and PhishTank entries containing uppercase characters never match, since message URLs are lowercased while HTTP lookup entries keep their original case. HTTP lookups now match keys case-insensitively.
+ - URL shortener links are followed using the lowercased URL, so case-sensitive short links resolve to the wrong destination or not at all.
+ - Incremental training never advances its position past the first run, so every retained sample added since then is trained again, and counted again in the reservoir, on each run until it expires.
+ - Updating the rules only adds new objects, so upstream changes to existing rules, DNSBL servers, HTTP lookups, lookup keys and file extensions never reach an existing installation.
+ - Updating the rules reports success when objects fail to import, or when a configuration error stops the updated settings from being activated.
+- JMAP:
+ - A `PushSubscription` created within the verification rate limit window of another one on the same account never receives its `PushVerification`, since the blocked verification is dropped instead of being sent once the window expires.
+ - A push notification retried after a failed delivery can report an older state than a change queued during the failed attempt, since the older state changes are merged last and overwrite the newer ones.
+ - Changes made while a push request is in flight are not delivered until the next change reaches the same subscription, since a successful delivery cancels the pending retry.
+ - The VAPID `aud` claim is derived from a hand-written parse of the push URL, so a crafted push URL can make the server sign a token for a push service other than the one the request is sent to.
+ - `Email/import` rejects a `blobId` that refers to a `Blob/upload` creation id in the same request (`"#u0"`) with `Invalid blob id.`.
+ - `Email/set` with a full `mailboxIds` object identical to the current mailboxes, together with a keyword change, stores the message with IMAP UID 0, so IMAP clients stop seeing it.
+- MTA:
+ - A node without the `outboundMta` role stops replying to `DATA` and to JMAP submissions once about 1024 messages have been queued on it.
+ - MX records are resolved through the DNSSEC-validating resolver even when DANE is disabled.
+ - A `DATA` stage Sieve script does not see headers added by milters or MTA hooks, and discards every milter and MTA hook change when it edits the message.
+- MySQL: Range deletions and search index removals start with a single unbounded `DELETE` and switch to chunks only after a timeout.
+- IMAP: `COPY` and `MOVE` fail with `NO [CONTACTADMIN]` when another session changes the same message at the same time.
+- Autodiscover: Implicit TLS ports (993, 995, 465) are advertised with `TLS`, which Outlook reads as STARTTLS.
+- HTTP: Idle keep-alive connections are never closed.
+
## [0.16.23] - 2026-09-21
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.
diff --git a/Cargo.lock b/Cargo.lock
index 396def3..c6e73fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,7 +116,7 @@ dependencies = [
"once_cell",
"serde",
"version_check",
- "zerocopy 0.8.57",
+ "zerocopy 0.8.59",
]
[[package]]
@@ -221,7 +221,7 @@ dependencies = [
"nom",
"num-traits",
"rusticata-macros",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
]
@@ -318,7 +318,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tokio-rustls",
"tokio-stream",
@@ -412,7 +412,7 @@ dependencies = [
"quick-xml 0.38.4",
"rust-ini",
"serde",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
"url",
]
@@ -447,7 +447,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0"
dependencies = [
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -740,16 +740,6 @@ dependencies = [
"wyz",
]
-[[package]]
-name = "bitvec-nom2"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0"
-dependencies = [
- "bitvec",
- "nom",
-]
-
[[package]]
name = "blake2"
version = "0.10.6"
@@ -882,7 +872,7 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_urlencoded",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
"tokio",
"tokio-stream",
@@ -1110,9 +1100,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.4.7"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
+checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1302,7 +1292,7 @@ dependencies = [
[[package]]
name = "common"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"aes-gcm-siv",
"ahash",
@@ -1487,7 +1477,7 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "coordinator"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"async-nats",
"futures",
@@ -1899,7 +1889,7 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
[[package]]
name = "dav"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"calcard",
"chrono",
@@ -1922,7 +1912,7 @@ dependencies = [
[[package]]
name = "dav-proto"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"calcard",
"chrono",
@@ -2022,7 +2012,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -2135,7 +2125,7 @@ dependencies = [
[[package]]
name = "directory"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"argon2 0.6.0",
@@ -2206,16 +2196,16 @@ dependencies = [
[[package]]
name = "dns-update"
-version = "0.5.8"
+version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "887fbd45d910c4dd5808f65c0cd686d4f0f756450b9a6c7ecce4a9f450657509"
+checksum = "5b55d6bab7e8f88b1ff3079da050676de0fbc0589014bb52bf39975affb8ad19"
dependencies = [
"aws-lc-rs",
"base64 0.23.1",
- "chrono",
"hex",
"hickory-net",
"hickory-proto",
+ "jiff",
"quick-xml 0.42.0",
"reqwest 0.13.5",
"rustls",
@@ -2315,7 +2305,7 @@ dependencies = [
"openssl",
"serde",
"sha2 0.10.9",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -2376,7 +2366,7 @@ dependencies = [
[[package]]
name = "email"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"aes 0.9.3",
"aes-gcm 0.11.1",
@@ -2418,13 +2408,12 @@ dependencies = [
[[package]]
name = "encoding_rs"
-version = "0.8.41"
+version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a"
+checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
dependencies = [
"cfg-if",
"core_detect",
- "multiversion",
"multiversion_no_op",
"rustversion",
"scopeguard",
@@ -2485,7 +2474,7 @@ dependencies = [
[[package]]
name = "event_macro"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"quote",
"syn 3.0.6",
@@ -2594,9 +2583,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
-version = "0.1.13"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fixed_decimal"
@@ -2684,7 +2673,7 @@ dependencies = [
"memchr",
"mime",
"serde",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tracing",
]
@@ -3013,7 +3002,7 @@ dependencies = [
[[package]]
name = "groupware"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"calcard",
@@ -3102,8 +3091,6 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
- "allocator-api2",
- "equivalent",
"foldhash 0.2.0",
]
@@ -3173,7 +3160,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
"tinyvec",
"tokio",
@@ -3199,7 +3186,7 @@ dependencies = [
"rand 0.10.3",
"ring",
"rustls-pki-types",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
"tinyvec",
"tracing",
@@ -3228,7 +3215,7 @@ dependencies = [
"rustls",
"smallvec",
"system-configuration",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tokio-rustls",
"tracing",
@@ -3302,7 +3289,7 @@ dependencies = [
[[package]]
name = "http"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"async-stream",
"base64 0.23.1",
@@ -3398,7 +3385,7 @@ dependencies = [
[[package]]
name = "http_proto"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"common",
"compact_str",
@@ -3518,16 +3505,17 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.20"
+version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
dependencies = [
- "base64 0.22.1",
+ "base64 0.23.1",
"bytes",
"futures-channel",
"futures-util",
"http 1.5.0",
"http-body",
+ "httparse",
"hyper",
"ipnet",
"libc",
@@ -3836,9 +3824,9 @@ dependencies = [
[[package]]
name = "icu_time_data"
-version = "2.3.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "005ff86fa5851a77fc0a977b72d717f777bd9230415a247219d7898aefb1517a"
+checksum = "d9e21be527807cda7562cbfbdb04179f9cca46c12691d6a8073ffae6cfd7aeec"
[[package]]
name = "idea"
@@ -3884,7 +3872,7 @@ checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
[[package]]
name = "imap"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"common",
@@ -3909,7 +3897,7 @@ dependencies = [
[[package]]
name = "imap_proto"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"base64 0.23.1",
@@ -3924,7 +3912,7 @@ dependencies = [
[[package]]
name = "inbuxa"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"common",
"coordinator",
@@ -3932,7 +3920,7 @@ dependencies = [
"directory",
"email",
"groupware",
- "http 0.16.23",
+ "http 0.16.24",
"http_proto",
"imap",
"jmap",
@@ -4214,7 +4202,7 @@ dependencies = [
[[package]]
name = "jmap"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"async-stream",
"base64 0.23.1",
@@ -4297,7 +4285,7 @@ dependencies = [
[[package]]
name = "jmap_proto"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"calcard",
@@ -4326,7 +4314,7 @@ dependencies = [
"jni-sys",
"log",
"simd_cesu8",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"walkdir",
"windows-link 0.2.1",
]
@@ -4375,9 +4363,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.105"
+version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
@@ -4540,7 +4528,7 @@ dependencies = [
"percent-encoding",
"rustls",
"rustls-native-certs",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tokio-rustls",
"tokio-stream",
@@ -4603,9 +4591,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
-version = "0.1.24"
+version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a"
+checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
dependencies = [
"libc",
]
@@ -4684,9 +4672,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
-version = "0.18.4"
+version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25"
+checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
dependencies = [
"hashbrown 0.17.1",
]
@@ -4803,7 +4791,7 @@ dependencies = [
[[package]]
name = "managesieve"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"common",
"compact_str",
@@ -4938,7 +4926,7 @@ checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492"
[[package]]
name = "migration"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"common",
"email",
@@ -5051,27 +5039,6 @@ dependencies = [
"uuid",
]
-[[package]]
-name = "multiversion"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c"
-dependencies = [
- "multiversion-macros",
-]
-
-[[package]]
-name = "multiversion-macros"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83"
-dependencies = [
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 3.0.6",
-]
-
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
@@ -5113,7 +5080,7 @@ dependencies = [
"quote",
"syn 2.0.119",
"termcolor",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -5138,7 +5105,7 @@ dependencies = [
"rustls",
"serde",
"socket2 0.6.5",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -5170,7 +5137,7 @@ dependencies = [
"serde_json",
"sha1 0.10.7",
"sha2 0.10.9",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"uuid",
]
@@ -5209,7 +5176,7 @@ dependencies = [
[[package]]
name = "nlp"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"hashify",
@@ -5536,7 +5503,7 @@ dependencies = [
"futures-sink",
"js-sys",
"pin-project-lite",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tracing",
]
@@ -5565,7 +5532,7 @@ dependencies = [
"opentelemetry_sdk",
"prost",
"reqwest 0.13.5",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tonic",
"tonic-types",
@@ -5600,7 +5567,7 @@ dependencies = [
"percent-encoding",
"portable-atomic",
"rand 0.9.5",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -5787,9 +5754,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
-version = "2.9.1"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad"
+checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8"
dependencies = [
"memchr",
"ucd-trie",
@@ -5797,9 +5764,9 @@ dependencies = [
[[package]]
name = "pest_derive"
-version = "2.9.1"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f"
+checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf"
dependencies = [
"pest",
"pest_generator",
@@ -5807,9 +5774,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.9.1"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5"
+checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109"
dependencies = [
"pest",
"pest_meta",
@@ -5820,9 +5787,9 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.9.1"
+version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e"
+checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb"
dependencies = [
"pest",
]
@@ -6041,7 +6008,7 @@ dependencies = [
[[package]]
name = "pop3"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"common",
"directory",
@@ -6132,7 +6099,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
- "zerocopy 0.8.57",
+ "zerocopy 0.8.59",
]
[[package]]
@@ -6173,9 +6140,9 @@ dependencies = [
[[package]]
name = "privdrop"
-version = "0.5.6"
+version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70722a5a3728c9603c8d9469b64b8d1ee54dae6d74e24146da7f501b4c76540f"
+checksum = "454e2cdcd17f5b868a2f600b14660c1af558b85b69540b15ddc6b0a890d5b7f0"
dependencies = [
"libc",
"nix",
@@ -6243,7 +6210,7 @@ dependencies = [
"lazy_static",
"memchr",
"parking_lot",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -6280,9 +6247,9 @@ dependencies = [
[[package]]
name = "proxy-header"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc1493f63ddddfba840c3169e997c2905d09538ace72d64e84af6324c6e0e065"
+checksum = "accf7ba1ae5e4f5b6acfb2d88d3cf68d03c333fca0260b06fb9aafcfcf22e2f5"
dependencies = [
"pin-project-lite",
"tokio",
@@ -6290,9 +6257,9 @@ dependencies = [
[[package]]
name = "psl"
-version = "2.1.235"
+version = "2.1.238"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8319b56ff38ca0522b4e1e40bfa2b5de7f62dc89fc1e9033eac365551ec58e0e"
+checksum = "2e6aa2cd4e8e062e78ac5d7df6206e2f8e39b624f658efd6f34f9aa1928ac5da"
dependencies = [
"psl-types",
]
@@ -6403,7 +6370,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.6.5",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tracing",
"web-time",
@@ -6428,7 +6395,7 @@ dependencies = [
"rustls-pki-types",
"rustls-platform-verifier",
"slab",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tinyvec",
"tracing",
"web-time",
@@ -6629,12 +6596,11 @@ dependencies = [
[[package]]
name = "rasn"
-version = "0.28.14"
+version = "0.28.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d1b71dd951343df0fe30b11bca09518f3aba8e5bd0ece4564adbc2dabd875fa"
+checksum = "9ff8f77a3a7082ed9f3e878927f34b1d633293fce9363d8ba14ee1c24bd2e967"
dependencies = [
"bitvec",
- "bitvec-nom2",
"bytes",
"cfg-if",
"chrono",
@@ -6652,9 +6618,9 @@ dependencies = [
[[package]]
name = "rasn-cms"
-version = "0.28.14"
+version = "0.28.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b4abf4c2fe5537b99e2bf3099a7ad26e40bd9cf51bc003600ed58dbbff69a1c"
+checksum = "2d11eca85a3699d3e52795874704ddd6d2f03bda3585be3e6655a0d9a166e84e"
dependencies = [
"rasn",
"rasn-pkix",
@@ -6662,9 +6628,9 @@ dependencies = [
[[package]]
name = "rasn-derive"
-version = "0.28.14"
+version = "0.28.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "979eafa601d351f7f6490f1d2f4decc168e8e51cb6517c6c6ff80111951129d5"
+checksum = "6fe71aa61cfb0191e50b95ebd72d454e9eeabff8a35b6252b99f2e90b7ef4c6d"
dependencies = [
"proc-macro2",
"rasn-derive-impl",
@@ -6673,9 +6639,9 @@ dependencies = [
[[package]]
name = "rasn-derive-impl"
-version = "0.28.14"
+version = "0.28.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eee3136c97d6f2553c0d9f84d2c2555bb87ae9b365c5c9274e8bc5c366174431"
+checksum = "ac6b7d207ad810df71b46ef90916dbb71fe366e0ad58a62e737fe6098525684c"
dependencies = [
"either",
"itertools 0.13.0",
@@ -6687,9 +6653,9 @@ dependencies = [
[[package]]
name = "rasn-pkix"
-version = "0.28.14"
+version = "0.28.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c029da7ed3b94dd93b75299431984081c1eaf88ce79771c21b056f0ebf6fb964"
+checksum = "773eb443e9c58d9d356c065bbfea2c04322277c44073b4e0716967d8ccd7ef58"
dependencies = [
"rasn",
]
@@ -6762,9 +6728,9 @@ dependencies = [
[[package]]
name = "redis"
-version = "1.7.0"
+version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2acbc41a996f7652b2ddd9dfd98cc4ff602cfd742ae35382f07f608405ab50ed"
+checksum = "ed5b98ae99461e02895c3a96a25dbe4d852a3d5e5b815d31d5d9e4b5344d99e9"
dependencies = [
"arcstr",
"async-lock",
@@ -6808,7 +6774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
dependencies = [
"libredox",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -6862,7 +6828,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "registry"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"hashify",
@@ -7117,7 +7083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
dependencies = [
"hashbrown 0.16.1",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -7178,7 +7144,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"sysinfo",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
"tokio",
"tokio-stream",
@@ -7287,9 +7253,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier"
-version = "0.7.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
@@ -7308,9 +7274,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier-android"
-version = "0.1.1"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
[[package]]
name = "rustls-webpki"
@@ -7419,7 +7385,7 @@ dependencies = [
[[package]]
name = "scim"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"base64 0.23.1",
@@ -7445,7 +7411,7 @@ dependencies = [
[[package]]
name = "scim-proto"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"hashify",
"serde",
@@ -7582,7 +7548,7 @@ dependencies = [
"sha2 0.10.9",
"sha3 0.10.9",
"slh-dsa",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"twofish",
"typenum",
"x25519-dalek",
@@ -7771,7 +7737,7 @@ dependencies = [
[[package]]
name = "services"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"aes-gcm 0.11.1",
"aho-corasick",
@@ -8041,15 +8007,15 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
dependencies = [
"num-bigint 0.4.8",
"num-traits",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
]
[[package]]
name = "siphasher"
-version = "1.0.3"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]]
name = "slab"
@@ -8078,13 +8044,13 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.16.1"
+version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
[[package]]
name = "smtp"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"base64 0.23.1",
@@ -8175,7 +8141,7 @@ dependencies = [
[[package]]
name = "spam-filter"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"common",
"compact_str",
@@ -8295,7 +8261,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "store"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"arc-swap",
@@ -8546,7 +8512,7 @@ dependencies = [
"serde",
"serde_json",
"serde_with",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tokio-stream",
"tokio-util",
@@ -8555,7 +8521,7 @@ dependencies = [
[[package]]
name = "tests"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"aws-lc-rs",
@@ -8577,7 +8543,7 @@ dependencies = [
"form_urlencoded",
"futures",
"groupware",
- "http 0.16.23",
+ "http 0.16.24",
"http_proto",
"hyper",
"hyper-util",
@@ -8639,11 +8605,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.20"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
- "thiserror-impl 2.0.20",
+ "thiserror-impl 2.0.21",
]
[[package]]
@@ -8659,9 +8625,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.20"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
@@ -8762,7 +8728,7 @@ checksum = "1461056cc1ef47003f7ee16e4cef3741068d4c7f6b627bfce49b7c00c120a530"
dependencies = [
"futures-util",
"pin-project-lite",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tokio-rustls",
]
@@ -9149,7 +9115,7 @@ dependencies = [
[[package]]
name = "trc"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"base64 0.23.1",
@@ -9194,7 +9160,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"sha1 0.10.7",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -9210,7 +9176,7 @@ dependencies = [
"log",
"rand 0.10.3",
"sha1 0.11.0",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
]
[[package]]
@@ -9258,7 +9224,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "types"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"blake3",
"compact_str",
@@ -9427,7 +9393,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utils"
-version = "0.16.23"
+version = "0.16.24"
dependencies = [
"ahash",
"arcstr",
@@ -9589,9 +9555,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.128"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
@@ -9602,19 +9568,20 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.78"
+version = "0.4.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
dependencies = [
"js-sys",
+ "tokio",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.128"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -9622,9 +9589,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.128"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -9635,9 +9602,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.128"
+version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
dependencies = [
"unicode-ident",
]
@@ -9670,9 +9637,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.105"
+version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -10074,7 +10041,7 @@ dependencies = [
"oid-registry",
"ring",
"rusticata-macros",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"time",
]
@@ -10628,11 +10595,11 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.57"
+version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
+checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
dependencies = [
- "zerocopy-derive 0.8.57",
+ "zerocopy-derive 0.8.59",
]
[[package]]
@@ -10648,9 +10615,9 @@ dependencies = [
[[package]]
name = "zerocopy-derive"
-version = "0.8.57"
+version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
+checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
dependencies = [
"proc-macro2",
"quote",
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml
index 37bd7d1..e305a13 100644
--- a/crates/common/Cargo.toml
+++ b/crates/common/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "common"
-version = "0.16.23"
+version = "0.16.24"
edition = "2024"
build = "build.rs"
diff --git a/crates/common/src/manager/defaults.rs b/crates/common/src/manager/defaults.rs
index 5f759b4..7a5e964 100644
--- a/crates/common/src/manager/defaults.rs
+++ b/crates/common/src/manager/defaults.rs
@@ -570,8 +570,8 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
// inbuxa: rules are always to hand, since a copy ships with the server
// (spam_rules). They load on first boot, and again when the bundled
- // version differs from the one last loaded, which only adds what's
- // missing: new tags and rules, never a changed score.
+ // rules differ from the ones last loaded: new tags and rules, fixes to
+ // rules nobody edited, never a changed score or an admin's edit.
let rules_url = super::spam_rules::rules_url(
bp.registry
.object::(Id::singleton())
@@ -582,7 +582,7 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
&& super::spam_rules::applied_version(&bp.data_store)
.await?
.as_deref()
- != Some(super::spam_rules::BUNDLED_SPAM_RULES_VERSION);
+ != Some(super::spam_rules::BUNDLED_SPAM_RULES_APPLIED);
if bp.registry.count_object(ObjectType::SpamRule).await? == 0 || bundled_is_new {
let mut batch = BatchBuilder::new();
batch.schedule_task(Task::SpamFilterMaintenance(TaskSpamFilterMaintenance {
diff --git a/crates/common/src/manager/spam_rules.rs b/crates/common/src/manager/spam_rules.rs
index acac5e2..050540c 100644
--- a/crates/common/src/manager/spam_rules.rs
+++ b/crates/common/src/manager/spam_rules.rs
@@ -12,11 +12,16 @@
//! and license) and uses it whenever no other source is configured. The rules
//! URL remains an operator override (`https://` or `file://`).
//!
-//! Loading rules only ever adds what's missing, never changes an existing rule
-//! or score. They load on first boot, and again whenever the bundled version
-//! differs from the one last applied, so an upgrade brings new tags (the AI
-//! classifier's `LLM_*` scores, say) to an install that already had rules.
+//! Loading rules adds what's missing and brings an existing rule up to date,
+//! but never touches one an admin edited: every object an update writes is
+//! fingerprinted, and one that no longer matches its fingerprint is kept as
+//! it is. Tags (scores) are never replaced. Switching a rule on or off isn't
+//! an edit, and is kept either way. They load on first boot, and again
+//! whenever the bundled rules differ from the ones last applied, so an
+//! upgrade brings new tags (the AI classifier's `LLM_*` scores, say) and
+//! fixed rules to an install that already had rules.
+use registry::{schema::prelude::ObjectType, types::EnumImpl};
use std::io::Read;
use store::{
SUBSPACE_INBUXA, Store, ValueKey,
@@ -27,13 +32,17 @@ use trc::AddContext;
/// The version of spam-filter the embedded rules come from.
pub const BUNDLED_SPAM_RULES_VERSION: &str = "3.0.2";
+/// What's recorded once the bundled rules are loaded: their version, then the
+/// fork's own generation of the update, so a change to how an update applies
+/// runs it once more. Generation 2 fingerprints (upstream v0.16.24).
+pub const BUNDLED_SPAM_RULES_APPLIED: &str = "3.0.2+2";
+
static BUNDLED_SPAM_RULES: &[u8] =
include_bytes!("../../../../resources/spam-filter/spam-filter-rules.json.gz");
/// Upstream's default rules source, the value every install created before
/// the rules were bundled has saved. Read only to treat it as unset.
-const LEGACY_DEFAULT_URL: &str =
- "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz";
+const LEGACY_DEFAULT_URL: &str = "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz";
/// The URL to fetch rules from, or `None` for the bundled rules. An empty
/// setting and upstream's old default both mean the bundled rules.
@@ -57,14 +66,49 @@ fn applied_key() -> ValueClass {
})
}
-/// The bundled version last loaded into the registry, if any.
+fn fingerprint_key(object: ObjectType, id: u64) -> ValueClass {
+ let mut key = b"Sf".to_vec();
+ key.extend_from_slice(object.as_str().as_bytes());
+ key.push(0);
+ key.extend_from_slice(&id.to_be_bytes());
+ ValueClass::Any(AnyClass {
+ subspace: SUBSPACE_INBUXA,
+ key,
+ })
+}
+
+/// The fingerprint of what a rules update last wrote to this object, if one
+/// did.
+pub async fn fingerprint(data: &Store, object: ObjectType, id: u64) -> trc::Result