From 1fd32c51a09f45e24a7c9fcb7a10f990160d22f6 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 12 Oct 2026 07:20:53 -0700 Subject: [PATCH 1/2] Import upstream v0.16.26, stripped Upstream commit: 18f4e9505027d790fd7e9fdad7f922be6719101d Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 118 in 50 files Dangling module declarations removed: 5 Edits turning enterprise off: 25 Third-party code: 14 files, 0 not in THIRD-PARTY.md Renamed identifiers: 67 in 19 files Verification: clean The same Enterprise footprint as v0.16.25. The build check is clean apart from the expected errors in the rebuilt-feature tests. Adds sd_notify readiness, an x86-64-v2 baseline for glibc x86_64 builds with a startup CPU check, and hickory from upstream's patched branch. Fixes RocksDB blob GC rewriting every blob on compaction, a milter stall with SMFIP_NOHDRS, OIDC's email-claim fallback, Mailbox/set moves and case-only renames, EmailSubmission onSuccess* naming an existing submission, and VacationResponse/set enabling an empty script. --- .cargo/config.toml | 2 + .dockerignore | 1 + .github/workflows/ci.yml | 39 ++- .gitignore | 1 + CHANGELOG.md | 19 ++ Cargo.lock | 261 ++++++++++---------- Cargo.toml | 5 + Dockerfile.build | 54 ++-- crates/common/Cargo.toml | 5 +- crates/common/src/config/smtp/resolver.rs | 2 - crates/common/src/manager/mod.rs | 2 + crates/common/src/manager/systemd.rs | 25 ++ crates/coordinator/Cargo.toml | 2 +- crates/dav-proto/Cargo.toml | 2 +- crates/dav/Cargo.toml | 2 +- crates/directory/Cargo.toml | 2 +- crates/directory/src/backend/oidc/lookup.rs | 8 +- crates/email/Cargo.toml | 2 +- crates/groupware/Cargo.toml | 2 +- crates/http-proto/Cargo.toml | 2 +- crates/http/Cargo.toml | 2 +- crates/imap-proto/Cargo.toml | 2 +- crates/imap/Cargo.toml | 2 +- crates/jmap-proto/Cargo.toml | 2 +- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/mailbox/set.rs | 16 +- crates/jmap/src/registry/mapping/action.rs | 9 +- crates/jmap/src/submission/set.rs | 90 +++---- crates/jmap/src/vacation/set.rs | 2 +- crates/main/Cargo.toml | 5 +- crates/main/src/cpu.rs | 51 ++++ crates/main/src/main.rs | 14 ++ crates/managesieve/Cargo.toml | 2 +- crates/migration/Cargo.toml | 2 +- crates/nlp/Cargo.toml | 2 +- crates/pop3/Cargo.toml | 2 +- crates/registry/Cargo.toml | 2 +- crates/scim-proto/Cargo.toml | 2 +- crates/scim/Cargo.toml | 2 +- crates/services/Cargo.toml | 2 +- crates/smtp/Cargo.toml | 2 +- crates/smtp/src/inbound/milter/client.rs | 3 +- crates/spam-filter/Cargo.toml | 2 +- crates/store/Cargo.toml | 4 +- crates/store/src/backend/rocksdb/main.rs | 3 +- crates/store/src/backend/rocksdb/write.rs | 33 ++- crates/trc/Cargo.toml | 2 +- crates/trc/event-macro/Cargo.toml | 2 +- crates/types/Cargo.toml | 2 +- crates/utils/Cargo.toml | 2 +- crates/utils/proc-macros/Cargo.toml | 2 +- docker-bake.hcl | 3 +- resources/systemd/stalwart-mail.service | 2 +- tests/Cargo.toml | 4 +- tests/docker/keycloak/stalwart-realm.json | 66 +++++ tests/src/jmap/mail/mailbox.rs | 31 +++ tests/src/jmap/mail/set.rs | 5 +- tests/src/jmap/mail/submission.rs | 111 +++++++++ tests/src/jmap/mail/vacation_response.rs | 37 +++ 59 files changed, 698 insertions(+), 269 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 crates/common/src/manager/systemd.rs create mode 100644 crates/main/src/cpu.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..059c0a65 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(all(target_arch = "x86_64", not(target_env = "musl")))'] +rustflags = ["-C", "target-cpu=x86-64-v2"] diff --git a/.dockerignore b/.dockerignore index 27780830..89819958 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,5 +6,6 @@ !tests !resources +!.cargo !Cargo.lock !Cargo.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06952484..30471e3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,39 +115,48 @@ jobs: matrix: include: - target: x86_64-unknown-linux-gnu + runner: ubuntu-latest platform: linux/amd64 suffix: "" build_env: "" - target: x86_64-unknown-linux-musl + runner: ubuntu-latest platform: linux/amd64 suffix: "-alpine" build_env: "" - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm platform: linux/arm64 suffix: "" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: aarch64-unknown-linux-musl + runner: ubuntu-24.04-arm platform: linux/arm64 suffix: "-alpine" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: armv7-unknown-linux-gnueabihf + runner: ubuntu-latest platform: linux/arm/v7 suffix: "" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: armv7-unknown-linux-musleabihf + runner: ubuntu-latest platform: linux/arm/v7 suffix: "-alpine" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: arm-unknown-linux-gnueabihf + runner: ubuntu-latest platform: linux/arm/v6 suffix: "" build_env: "" - target: arm-unknown-linux-musleabihf + runner: ubuntu-latest platform: linux/arm/v6 suffix: "-alpine" build_env: "" name: Build / ${{matrix.target}} - runs-on: ubuntu-latest + runs-on: ${{matrix.runner}} + timeout-minutes: 180 steps: - name: Checkout uses: actions/checkout@v7 @@ -163,23 +172,31 @@ jobs: - name: Add swap (heavy ARM targets) if: contains(matrix.target, 'arm') || contains(matrix.target, 'aarch64') run: | - mnt_avail=$(df --output=avail -k /mnt | tail -1) - if [ "$mnt_avail" -lt 18874368 ]; then - echo "Insufficient space on /mnt (${mnt_avail}K available), aborting swap setup" + swap_dir="" + for dir in /mnt /; do + if [ -d "$dir" ] && [ "$(df --output=avail -k "$dir" | tail -1)" -ge 18874368 ]; then + swap_dir="$dir" + break + fi + done + if [ -z "$swap_dir" ]; then + echo "Insufficient space for swap on /mnt or /, aborting swap setup" + df -h exit 1 fi - sudo fallocate -l 16G /mnt/swapfile - sudo chmod 600 /mnt/swapfile - sudo mkswap /mnt/swapfile - sudo swapon /mnt/swapfile + sudo fallocate -l 16G "${swap_dir%/}/swapfile" + sudo chmod 600 "${swap_dir%/}/swapfile" + sudo mkswap "${swap_dir%/}/swapfile" + sudo swapon "${swap_dir%/}/swapfile" sudo sysctl vm.swappiness=80 free -h swapon --show - name: Set up QEMU + if: startsWith(matrix.platform, 'linux/arm/') uses: docker/setup-qemu-action@v4 with: - platforms: "arm64,arm" + platforms: "arm" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -198,6 +215,7 @@ jobs: password: ${{github.token}} - name: Log In to DockerHub + if: github.event_name == 'push' || inputs.Docker uses: docker/login-action@v4 with: username: ${{secrets.DOCKERHUB_USERNAME}} @@ -215,7 +233,7 @@ jobs: path: | var-cache-apt var-lib-apt - key: apt-cache-${{ hashFiles('Dockerfile.build') }} + key: apt-cache-${{ runner.arch }}-${{ hashFiles('Dockerfile.build') }} - name: Cache Cargo uses: actions/cache@v6.1.0 @@ -258,6 +276,7 @@ jobs: uses: docker/bake-action@v7 env: DOCKER_BUILD_RECORD_UPLOAD: false + GITHUB_TOKEN: ${{github.token}} TARGET: ${{matrix.target}} GHCR_REPO: ghcr.io/${{github.repository}} BUILD_ENV: ${{matrix.build_env}} diff --git a/.gitignore b/.gitignore index d6d2a48a..1a302cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ run.sh !.gitignore !.gitattributes !.github +!.cargo CLAUDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d999d420..db262f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [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. + +## Added +- `sd_notify` support. +- 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 + +## Fixed +- DNSSEC: Use `hickory-resolver` patched fork at https://github.com/stalwartlabs/hickory-dns. +- RocksDB: Blob garbage collection rewrites every stored blob on each compaction of the blobs column family, about once per 128MB of new mail. +- Milter: End-of-headers is not sent to milters that negotiate `SMFIP_NOHDRS` without `SMFIP_NOEOH`, so the transaction stalls until it times out. +- OIDC: When the access token lacks the `claimUsername` claim but has an `email` claim, the account is built from `email` without consulting the userinfo endpoint. +- JMAP: + - `onSuccessUpdateEmail` and `onSuccessDestroyEmail` entries in `EmailSubmission/set` that name an existing submission by id update or destroy the Email with the same id instead of the submission's own Email. + - A `VacationResponse/set` that only sets `isEnabled` on an account without a vacation response activates an empty Sieve script. + ## [0.16.25] - 2026-10-05 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 35191f52..70662a6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.8.59", + "zerocopy 0.8.61", ] [[package]] @@ -188,6 +188,12 @@ dependencies = [ "password-hash 0.6.1", ] +[[package]] +name = "array-const-fn-init" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bcb85e548c05d407fa6faff46b750ba287714ef32afc0f5e15b4641ffd6affb" + [[package]] name = "array-init" version = "2.1.0" @@ -330,13 +336,13 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -402,14 +408,14 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-creds" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3b85155d265df828f84e53886ed9e427aed979dd8a39f5b8b2162c77e142d7" +checksum = "886acd98f2df6e988d5428e92cdf041792ffa87a494d2b5125982502ec726a9c" dependencies = [ "attohttpc", "home", "log", - "quick-xml 0.38.4", + "quick-xml 0.41.0", "rust-ini", "serde", "thiserror 2.0.21", @@ -443,9 +449,9 @@ dependencies = [ [[package]] name = "aws-region" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0" +checksum = "8f38493193e45cb4788ee7d479ee94a0a56721ccd28de79bbd0227bb8c11fee3" dependencies = [ "thiserror 2.0.21", ] @@ -1100,9 +1106,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "jobserver", @@ -1292,7 +1298,7 @@ dependencies = [ [[package]] name = "common" -version = "0.16.25" +version = "0.16.26" dependencies = [ "aes-gcm-siv", "ahash", @@ -1351,6 +1357,7 @@ dependencies = [ "rustls", "rustls-pemfile", "rustls-pki-types", + "sd-notify", "sequoia-openpgp", "serde", "serde_json", @@ -1476,7 +1483,7 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "coordinator" -version = "0.16.25" +version = "0.16.26" dependencies = [ "async-nats", "futures", @@ -1489,16 +1496,6 @@ dependencies = [ "zenoh", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1722,9 +1719,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", ] @@ -1888,7 +1885,7 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "dav" -version = "0.16.25" +version = "0.16.26" dependencies = [ "calcard", "chrono", @@ -1911,7 +1908,7 @@ dependencies = [ [[package]] name = "dav-proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "calcard", "chrono", @@ -2125,7 +2122,7 @@ dependencies = [ [[package]] name = "directory" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "argon2 0.6.0", @@ -2335,9 +2332,9 @@ dependencies = [ [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "elliptic-curve" @@ -2366,7 +2363,7 @@ dependencies = [ [[package]] name = "email" -version = "0.16.25" +version = "0.16.26" dependencies = [ "aes 0.9.3", "aes-gcm 0.11.1", @@ -2407,9 +2404,9 @@ dependencies = [ [[package]] name = "encodify" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798c447647dd23f673748f2b868ef309a01dd86aaae999182559d36f06ac82f0" +checksum = "e95d6e27686e1d6991a3077f4dbec9d33d18903466d234165ad86cd08c3ebc79" dependencies = [ "memchr", "simdutf8", @@ -2483,7 +2480,7 @@ dependencies = [ [[package]] name = "event_macro" -version = "0.16.25" +version = "0.16.26" dependencies = [ "quote", "syn 3.0.6", @@ -3011,7 +3008,7 @@ dependencies = [ [[package]] name = "groupware" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "calcard", @@ -3038,9 +3035,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" dependencies = [ "atomic-waker", "bytes", @@ -3144,8 +3141,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" +source = "git+https://github.com/stalwartlabs/hickory-dns?branch=stalwart%2F0.26#f8b92b17fa6c7d09a6959672592106ca1410937b" dependencies = [ "async-trait", "aws-lc-rs", @@ -3180,8 +3176,7 @@ dependencies = [ [[package]] name = "hickory-proto" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" +source = "git+https://github.com/stalwartlabs/hickory-dns?branch=stalwart%2F0.26#f8b92b17fa6c7d09a6959672592106ca1410937b" dependencies = [ "aws-lc-rs", "bitflags 2.13.2", @@ -3204,8 +3199,7 @@ dependencies = [ [[package]] name = "hickory-resolver" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" +source = "git+https://github.com/stalwartlabs/hickory-dns?branch=stalwart%2F0.26#f8b92b17fa6c7d09a6959672592106ca1410937b" dependencies = [ "cfg-if", "futures-util", @@ -3297,7 +3291,7 @@ dependencies = [ [[package]] name = "http" -version = "0.16.25" +version = "0.16.26" dependencies = [ "async-stream", "base64 0.23.1", @@ -3391,7 +3385,7 @@ dependencies = [ [[package]] name = "http_proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "compact_str", @@ -3445,9 +3439,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", @@ -3878,7 +3872,7 @@ checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a" [[package]] name = "imap" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "common", @@ -3902,7 +3896,7 @@ dependencies = [ [[package]] name = "imap_proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -4101,9 +4095,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" dependencies = [ "defmt", "jiff-core", @@ -4128,9 +4122,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" dependencies = [ "jiff-core", "proc-macro2", @@ -4140,9 +4134,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" [[package]] name = "jiff-tzdb-platform" @@ -4155,7 +4149,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.16.25" +version = "0.16.26" dependencies = [ "async-stream", "base64 0.23.1", @@ -4236,7 +4230,7 @@ dependencies = [ [[package]] name = "jmap_proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "calcard", @@ -4496,9 +4490,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libflate" @@ -4669,7 +4663,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226" dependencies = [ - "twox-hash 2.1.4", + "twox-hash 2.1.5", ] [[package]] @@ -4741,7 +4735,7 @@ dependencies = [ [[package]] name = "managesieve" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "compact_str", @@ -4876,7 +4870,7 @@ checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "migration" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "email", @@ -4922,9 +4916,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -5059,7 +5053,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "twox-hash 2.1.4", + "twox-hash 2.1.5", "url", "webpki-roots 1.0.9", ] @@ -5126,7 +5120,7 @@ dependencies = [ [[package]] name = "nlp" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "hashify", @@ -5958,7 +5952,7 @@ dependencies = [ [[package]] name = "pop3" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "directory", @@ -6033,9 +6027,9 @@ dependencies = [ [[package]] name = "powerfmt" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "ppmd-rust" @@ -6049,7 +6043,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.59", + "zerocopy 0.8.61", ] [[package]] @@ -6060,13 +6054,15 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prefix-trie" -version = "0.8.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +checksum = "6369af434551b9bf8be05ab871ac1263687d6f0c9485e6d781f904f3e06d7d03" dependencies = [ + "array-const-fn-init", "either", "ipnet", "num-traits", + "smallvec", ] [[package]] @@ -6207,9 +6203,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.238" +version = "2.1.241" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6aa2cd4e8e062e78ac5d7df6206e2f8e39b624f658efd6f34f9aa1928ac5da" +checksum = "c7319b480e204d67e759cad7886178335d26324c7c8573c126cc9dfa7350d064" dependencies = [ "psl-types", ] @@ -6265,16 +6261,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -6282,6 +6268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", + "serde", ] [[package]] @@ -6319,7 +6306,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.5", + "socket2 0.5.10", "thiserror 2.0.21", "tokio", "tracing", @@ -6360,7 +6347,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2 0.5.10", "tracing", "windows-sys 0.61.2", ] @@ -6778,7 +6765,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "registry" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "hashify", @@ -7069,9 +7056,9 @@ dependencies = [ [[package]] name = "rust-s3" -version = "0.37.2" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeedb13abdaa7e48d391de05b0569b37fa0a7a64a668dff6ffb2141ad0c2527e" +checksum = "15d4ed794cdfede6c59f81d485cae6bb63f47a7e35d190148d40754f783073c4" dependencies = [ "async-trait", "aws-creds", @@ -7087,7 +7074,7 @@ dependencies = [ "maybe-async", "md5", "percent-encoding", - "quick-xml 0.38.4", + "quick-xml 0.41.0", "reqwest 0.12.28", "serde", "serde_derive", @@ -7201,7 +7188,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -7329,7 +7316,7 @@ dependencies = [ [[package]] name = "scim" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "directory", @@ -7351,7 +7338,7 @@ dependencies = [ [[package]] name = "scim-proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "hashify", "serde", @@ -7377,6 +7364,15 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "sd-notify" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4ef7359e694bfaf1dd27a30f9d760b54c00dfae9f19bd0c05a39bc9128fe76" +dependencies = [ + "libc", +] + [[package]] name = "sec1" version = "0.7.3" @@ -7409,7 +7405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.13.2", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -7488,7 +7484,7 @@ dependencies = [ "sha2 0.10.9", "sha3 0.10.9", "slh-dsa", - "thiserror 2.0.21", + "thiserror 1.0.69", "twofish", "typenum", "x25519-dalek", @@ -7677,7 +7673,7 @@ dependencies = [ [[package]] name = "services" -version = "0.16.25" +version = "0.16.26" dependencies = [ "aes-gcm 0.11.1", "aho-corasick", @@ -7990,7 +7986,7 @@ checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "smtp" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -8080,7 +8076,7 @@ dependencies = [ [[package]] name = "spam-filter" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "compact_str", @@ -8193,7 +8189,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stalwart" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "coordinator", @@ -8201,10 +8197,11 @@ dependencies = [ "directory", "email", "groupware", - "http 0.16.25", + "http 0.16.26", "http_proto", "imap", "jmap", + "libc", "managesieve", "migration", "pop3", @@ -8231,7 +8228,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "store" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "arc-swap", @@ -8409,12 +8406,12 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" dependencies = [ "bitflags 2.13.2", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys", ] @@ -8491,7 +8488,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "aws-lc-rs", @@ -8513,7 +8510,7 @@ dependencies = [ "form_urlencoded", "futures", "groupware", - "http 0.16.25", + "http 0.16.26", "http_proto", "hyper", "hyper-util", @@ -8713,9 +8710,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -8852,18 +8849,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.15+spec-1.1.0" +version = "0.25.16+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +checksum = "88f38f3af8bf71841c67a990f4e2b52143b2d0a1a994c72513ff4c14977fc722" dependencies = [ "indexmap 2.14.2", "toml_datetime", @@ -8873,9 +8870,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333" dependencies = [ "winnow", ] @@ -9082,7 +9079,7 @@ dependencies = [ [[package]] name = "trc" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -9167,9 +9164,9 @@ dependencies = [ [[package]] name = "twox-hash" -version = "2.1.4" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" [[package]] name = "typed-path" @@ -9191,7 +9188,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "types" -version = "0.16.25" +version = "0.16.26" dependencies = [ "blake3", "compact_str", @@ -9225,9 +9222,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-bidi" @@ -9360,7 +9357,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utils" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "arcstr", @@ -9391,9 +9388,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.26.1" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -9474,9 +9471,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -10562,11 +10559,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.59" +version = "0.8.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866" dependencies = [ - "zerocopy-derive 0.8.59", + "zerocopy-derive 0.8.61", ] [[package]] @@ -10582,9 +10579,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.59" +version = "0.8.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759" dependencies = [ "proc-macro2", "quote", @@ -10614,9 +10611,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 20b326e4..c1218df3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,3 +77,8 @@ incremental = false debug-assertions = false overflow-checks = false rpath = false + +[patch.crates-io] +hickory-proto = { git = "https://github.com/stalwartlabs/hickory-dns", branch = "stalwart/0.26" } +hickory-net = { git = "https://github.com/stalwartlabs/hickory-dns", branch = "stalwart/0.26" } +hickory-resolver = { git = "https://github.com/stalwartlabs/hickory-dns", branch = "stalwart/0.26" } diff --git a/Dockerfile.build b/Dockerfile.build index b25f0126..2810a713 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -4,7 +4,7 @@ # ***************** # Base image for planner & builder # ***************** -FROM --platform=$BUILDPLATFORM rust:1.98.1-slim-trixie AS base +FROM --platform=$BUILDPLATFORM rust:slim-trixie AS base ENV DEBIAN_FRONTEND="noninteractive" \ BINSTALL_DISABLE_TELEMETRY=true \ @@ -24,25 +24,24 @@ RUN \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ apt-get update && \ apt-get install -yq --no-install-recommends ca-certificates curl git jq xz-utils make libclang-19-dev -# Zig is pinned to 0.13.0. Zig 0.14+ changed how its default target ABI is -# resolved for `x86_64-linux-gnu`-style triples, and on Stalwart's dep tree -# (rocksdb, jemalloc, aws-lc-sys, libfdb_c.so, rust-std 1.87+) this surfaces -# as ld.lld undefined references to pthread_*/stat/pow@GLIBC_2.33+. An -# explicit glibc suffix (`.2.17`) is NOT a workaround — C deps compiled via -# zig cc against Zig 0.16 headers still emit @2.33+ symbols even when the -# linker target is 2.17. Zig 0.13 produces binaries with a max glibc ref of -# ~2.28 which stays compatible with Debian 11 / RHEL 8. Revisit this pin if -# Zig upstream stabilizes a backwards-compatible default or if Stalwart drops -# its hard C deps. RUN \ - ZIG_VERSION=0.13.0 && \ - curl --retry 5 -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz" | tar -J -x -C /usr/local && \ - ln -s "/usr/local/zig-linux-$(uname -m)-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ + ZIG_VERSION=0.17.0 && \ + case "$(uname -m)" in \ + x86_64) ZIG_SHA256=1cbe9df9f27e6b78d14ccbca43b6703a404ef79ef1c463de901d7f088d4e2026 ;; \ + aarch64) ZIG_SHA256=9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8 ;; \ + *) exit 1 ;; \ + esac && \ + curl --retry 5 -fsSLo /tmp/zig.tar.xz "https://ziglang.org/download/${ZIG_VERSION}/zig-$(uname -m)-linux-${ZIG_VERSION}.tar.xz" && \ + echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c - && \ + tar -J -x -C /usr/local -f /tmp/zig.tar.xz && \ + rm /tmp/zig.tar.xz && \ + ln -s "/usr/local/zig-$(uname -m)-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ zig version # Install cargo-binstall RUN curl --retry 5 -fL --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash # Install cargo-chef & sccache & cargo-zigbuild -RUN cargo binstall --no-confirm cargo-chef@0.1.78 sccache@0.17.0 cargo-zigbuild@0.23.2 +RUN cargo binstall --no-confirm cargo-chef@0.1.78 sccache@0.17.0 +RUN cargo install --locked --git https://github.com/rust-cross/cargo-zigbuild --rev d9f7fd32fbac13cc26d23847835f7f23a5ed3a25 cargo-zigbuild # ***************** # Planner @@ -61,6 +60,7 @@ WORKDIR /app COPY --from=planner /app/recipe.json recipe.json ARG TARGET ARG BUILD_ENV +ARG GLIBC_VERSION=2.28 SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Install toolchain and specify some env variables RUN \ @@ -68,6 +68,13 @@ RUN \ rustup target add ${TARGET} && \ mkdir -p artifact && \ touch /env-cargo && \ + if [[ "${TARGET}" == *-gnu* ]]; then \ + echo "export BUILD_TARGET=${TARGET}.${GLIBC_VERSION}" >> /env-cargo; \ + mkdir -p target && \ + ln -s "${TARGET}" "target/${TARGET}.${GLIBC_VERSION}"; \ + else \ + echo "export BUILD_TARGET=${TARGET}" >> /env-cargo; \ + fi && \ if [ ! -z "${BUILD_ENV}" ]; then \ echo "export ${BUILD_ENV}" >> /env-cargo; \ echo "Setting up ${BUILD_ENV}"; \ @@ -85,15 +92,22 @@ RUN \ exit 1; \ ;; \ esac; \ + target_var="${TARGET^^}"; \ + echo "export FDB_RUSTFLAGS_VAR=CARGO_TARGET_${target_var//-/_}_RUSTFLAGS" >> /env-cargo; \ fi # Install FoundationDB (pinned to latest 7.4.x; Apple publishes 7.4 as # prereleases on GitHub, so the release list is fetched without the # prerelease filter and narrowed by tag name). ARG FDB_VERSION_RANGE="7.4" RUN \ + --mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - curl --retry 5 -fLso fdb-client.deb "$(curl --retry 5 -fLs 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \ + auth=() && \ + if [ -n "${GITHUB_TOKEN}" ]; then auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}"); fi && \ + fdb_url="$(curl --retry 5 -fLs "${auth[@]}" 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \ + [ -n "${fdb_url}" ] && \ + curl --retry 5 -fLso fdb-client.deb "${fdb_url}" && \ mkdir -p /fdb && \ dpkg -x fdb-client.deb /fdb && \ mv /fdb/usr/include/foundationdb /usr/include && \ @@ -108,7 +122,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats"; \ + env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats"; \ fi RUN \ --mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \ @@ -116,7 +130,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" + cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" # Copy the source code COPY . . ENV RUSTC_WRAPPER="sccache" \ @@ -129,7 +143,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats" && \ + env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats" && \ mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart-foundationdb; \ fi # Build generic version @@ -139,7 +153,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \ + cargo zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \ mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart # ***************** diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 59bbf8c2..a1c19319 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common" -version = "0.16.25" +version = "0.16.26" edition = "2024" build = "build.rs" @@ -85,6 +85,9 @@ quick-xml = "0.41" privdrop = "0.5.6" libc = "0.2.189" +[target.'cfg(target_os = "linux")'.dependencies] +sd-notify = "0.5" + [target.'cfg(windows)'.dependencies] socket2 = "0.6" diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index a23a51f8..cedf5606 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -214,7 +214,6 @@ impl Resolvers { let config_dnssec = resolver_config.clone(); let mut opts_dnssec = opts.clone(); opts_dnssec.validate = true; - opts_dnssec.num_concurrent_reqs = 1; let dnssec = DnssecResolver { resolver: TokioResolver::builder_with_config( @@ -344,7 +343,6 @@ impl Default for Resolvers { let config_dnssec = config.clone(); let mut opts_dnssec = opts.clone(); opts_dnssec.validate = true; - opts_dnssec.num_concurrent_reqs = 1; Self { dns: MessageAuthenticator::new(config, opts).expect("Failed to build DNS resolver"), diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs index 5f3ce6ff..6a990d5d 100644 --- a/crates/common/src/manager/mod.rs +++ b/crates/common/src/manager/mod.rs @@ -19,6 +19,8 @@ pub mod boot; pub mod console; pub mod defaults; pub mod restore; +#[cfg(target_os = "linux")] +pub mod systemd; pub const SPAM_TRAINER_KEY: &[u8] = "INBUXA_SPAM_TRAIN_DATA.lz4".as_bytes(); pub const SPAM_CLASSIFIER_KEY: &[u8] = "INBUXA_SPAM_CLASSIFIER_MODEL.lz4".as_bytes(); diff --git a/crates/common/src/manager/systemd.rs b/crates/common/src/manager/systemd.rs new file mode 100644 index 00000000..81200ac7 --- /dev/null +++ b/crates/common/src/manager/systemd.rs @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceState { + Ready, + Reloading, + Stopping, +} + +impl ServiceState { + pub fn notify(self) { + use sd_notify::{NotifyState, notify}; + + let _ = match self { + ServiceState::Ready => notify(&[NotifyState::Ready]), + ServiceState::Stopping => notify(&[NotifyState::Stopping]), + ServiceState::Reloading => NotifyState::monotonic_usec_now() + .and_then(|now| notify(&[NotifyState::Reloading, now])), + }; + } +} diff --git a/crates/coordinator/Cargo.toml b/crates/coordinator/Cargo.toml index e050d5e9..0ce5d2bf 100644 --- a/crates/coordinator/Cargo.toml +++ b/crates/coordinator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coordinator" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 550b2e10..fc38ace2 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav-proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index e59c6b7b..0d4943d9 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index d123cc55..fcd438d0 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "directory" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index 91ff6cb2..64d9012e 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -82,12 +82,14 @@ impl OpenIdDirectory { self.validate_scopes(&token_data.claims)?; let mut claims = token_data.claims; + let missing_username = + is_claim_missing(&claims, Some(&self.config.claim_email)); let jwt_email = self.resolve_email(&claims).ok(); let missing_profile = is_claim_missing(&claims, self.config.claim_name.as_ref()) || is_claim_missing(&claims, self.config.claim_groups.as_ref()); - if jwt_email.is_none() || missing_profile { + if jwt_email.is_none() || missing_username || missing_profile { match self.fetch_userinfo(token).await { Ok(userinfo) => { if let (Some(base), Value::Object(extra)) = @@ -106,8 +108,8 @@ impl OpenIdDirectory { } let email = match jwt_email { - Some(email) => email, - None => self.resolve_email(&claims)?, + Some(email) if !missing_username => email, + _ => self.resolve_email(&claims)?, }; return self.build_account(email, &claims); } diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index a7d5dce1..cabfdb24 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "email" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index 8cd18f64..5c35a7f3 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "groupware" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/http-proto/Cargo.toml b/crates/http-proto/Cargo.toml index 5de066d4..847e3765 100644 --- a/crates/http-proto/Cargo.toml +++ b/crates/http-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index 42e588f3..9ea40c04 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/imap-proto/Cargo.toml b/crates/imap-proto/Cargo.toml index 9331a57a..7be3049f 100644 --- a/crates/imap-proto/Cargo.toml +++ b/crates/imap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap_proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 8817c4bd..488bd7fd 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index bdba5357..3d57f317 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap_proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 61ccbd1a..65327dfe 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 430c469b..793fdc8b 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -587,14 +587,14 @@ impl MailboxSet for Server { if !changes.name.is_empty() { // Obtain parent mailbox id let lower_name = changes.name.to_lowercase(); - if update - .as_ref() - .is_none_or(|(_, m)| m.inner.name != changes.name) - && let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| { - m.name.to_lowercase() == lower_name - && m.parent_id().map_or(0, |id| id + 1) == changes.parent_id - }) - { + let current_id = update.as_ref().map(|(document_id, _)| *document_id); + if update.as_ref().is_none_or(|(_, m)| { + m.inner.name != changes.name || m.inner.parent_id != changes.parent_id + }) && let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| { + Some(m.document_id) != current_id + && m.parent_id().map_or(0, |id| id + 1) == changes.parent_id + && m.name.to_lowercase() == lower_name + }) { return Ok(Err(SetError::already_exists() .with_existing_id(Id::from(existing.document_id)) .with_description(format!( diff --git a/crates/jmap/src/registry/mapping/action.rs b/crates/jmap/src/registry/mapping/action.rs index 88ece999..b43ab9d4 100644 --- a/crates/jmap/src/registry/mapping/action.rs +++ b/crates/jmap/src/registry/mapping/action.rs @@ -5,6 +5,8 @@ */ use crate::registry::mapping::{RegistrySetResponse, map_bootstrap_error}; +#[cfg(target_os = "linux")] +use common::manager::systemd::ServiceState; use common::{ Server, config::mailstore::spamfilter::SpamFilterAction, @@ -86,8 +88,13 @@ pub(crate) async fn action_set( Action::ReloadBlockedIps => ObjectType::BlockedIp, _ => unreachable!(), }; + #[cfg(target_os = "linux")] + ServiceState::Reloading.notify(); let result = - Box::pin(set.server.reload_registry(RegistryChange::Reload(object))).await?; + Box::pin(set.server.reload_registry(RegistryChange::Reload(object))).await; + #[cfg(target_os = "linux")] + ServiceState::Ready.notify(); + let result = result?; if !result.has_errors() { set.server diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index 90d8b5c6..f6985158 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -75,6 +75,7 @@ impl EmailSubmissionSet for Server { // Process creates let mut success_email_ids = HashMap::new(); + let mut changed_email_ids = HashMap::new(); let mut batch = BatchBuilder::new(); for (id, object) in request.unwrap_create() { match self @@ -222,6 +223,8 @@ impl EmailSubmissionSet for Server { // Update record let mut new_submission = submission.inner.clone(); new_submission.undo_status = UndoStatus::Canceled; + let email_id = + Id::from_parts(new_submission.thread_id, new_submission.email_id); batch .with_account_id(account_id) .with_collection(Collection::EmailSubmission) @@ -233,6 +236,7 @@ impl EmailSubmissionSet for Server { ) .caused_by(trc::location!())? .commit_point(); + changed_email_ids.insert(id, email_id); response.updated.append(id, None); } else { response.not_updated.append( @@ -274,19 +278,21 @@ impl EmailSubmissionSet for Server { .await? { // Update record + let submission = submission + .to_unarchived::() + .caused_by(trc::location!())?; + let email_id = Id::from_parts( + u32::from(submission.inner.thread_id), + u32::from(submission.inner.email_id), + ); batch .with_account_id(account_id) .with_collection(Collection::EmailSubmission) .with_document(document_id) - .custom( - ObjectIndexBuilder::<_, ()>::new().with_current( - submission - .to_unarchived::() - .caused_by(trc::location!())?, - ), - ) + .custom(ObjectIndexBuilder::<_, ()>::new().with_current(submission)) .caused_by(trc::location!())? .commit_point(); + changed_email_ids.insert(id, email_id); response.destroyed.push(id); } else { response.not_destroyed.append(id, SetError::not_found()); @@ -304,18 +310,34 @@ impl EmailSubmissionSet for Server { } // On success - if (request + let resolve_email_id = |id: MaybeIdReference| match id { + MaybeIdReference::Id(id) => changed_email_ids.get(&id).copied(), + MaybeIdReference::Reference(id_ref) => success_email_ids.get(&id_ref).copied(), + MaybeIdReference::Invalid(_) => None, + }; + let update = request + .arguments + .on_success_update_email + .map(|update| { + update + .into_iter() + .filter_map(|(id, value)| { + resolve_email_id(id).map(|id| (MaybeInvalid::Value(id), value)) + }) + .collect::>() + }) + .filter(|update| !update.is_empty()); + let destroy = request .arguments .on_success_destroy_email - .as_ref() - .is_some_and(|p| !p.is_empty()) - || request - .arguments - .on_success_update_email - .as_ref() - .is_some_and(|p| !p.is_empty())) - && response.has_changes() - { + .map(|ids| { + ids.into_iter() + .filter_map(resolve_email_id) + .map(MaybeInvalid::Value) + .collect::>() + }) + .filter(|ids| !ids.is_empty()); + if update.is_some() || destroy.is_some() { *next_call = Call { id: String::new(), name: MethodName::new(MethodObject::Email, MethodFunction::Set), @@ -323,38 +345,8 @@ impl EmailSubmissionSet for Server { account_id: request.account_id, if_in_state: None, create: None, - update: request.arguments.on_success_update_email.map(|update| { - update - .into_iter() - .filter_map(|(id, value)| { - ( - match id { - MaybeIdReference::Id(id) => MaybeInvalid::Value(id), - MaybeIdReference::Reference(id_ref) => { - MaybeInvalid::Value(*(success_email_ids.get(&id_ref)?)) - } - MaybeIdReference::Invalid(id) => MaybeInvalid::Invalid(id), - }, - value, - ) - .into() - }) - .collect() - }), - destroy: request.arguments.on_success_destroy_email.map(|ids| { - MaybeResultReference::Value( - ids.into_iter() - .filter_map(|id| match id { - MaybeIdReference::Id(id) => Some(id), - MaybeIdReference::Reference(id_ref) => { - success_email_ids.get(&id_ref).copied() - } - MaybeIdReference::Invalid(_) => None, - }) - .map(MaybeInvalid::Value) - .collect(), - ) - }), + update, + destroy: destroy.map(MaybeResultReference::Value), arguments: Default::default(), }))), } diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index bcdb3261..44524906 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -142,7 +142,7 @@ impl VacationResponseSet for Server { // Parse properties let mut is_active = document_id.is_some_and(|id| active_script_id == Some(id)); - let mut build_script = create_id.is_some(); + let mut build_script = create_id.is_some() || sieve.blob_hash.is_empty(); let vacation = sieve.vacation_response.as_mut().unwrap(); for (property, mut value) in changes.into_expanded_object() { diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 47e8ff65..cc477755 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.16.25" +version = "0.16.26" edition = "2024" [[bin]] @@ -41,6 +41,9 @@ migration = { path = "../migration" } tokio = { version = "1.53", features = ["full"] } rustls = { version = "0.23.43", default-features = false, features = ["std", "aws_lc_rs", "tls12"] } +[target.'cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "freebsd")))'.dependencies] +libc = "0.2.189" + [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] tikv-jemallocator = "0.7" diff --git a/crates/main/src/cpu.rs b/crates/main/src/cpu.rs new file mode 100644 index 00000000..3c3ec5f8 --- /dev/null +++ b/crates/main/src/cpu.rs @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::arch::x86_64::__cpuid; + +const LEAF_FEATURES: u32 = 0x0000_0001; +const LEAF_EXTENDED_MAX: u32 = 0x8000_0000; +const LEAF_EXTENDED_FEATURES: u32 = 0x8000_0001; + +const SSE3: u32 = 1 << 0; +const SSSE3: u32 = 1 << 9; +const CMPXCHG16B: u32 = 1 << 13; +const SSE4_1: u32 = 1 << 19; +const SSE4_2: u32 = 1 << 20; +const POPCNT: u32 = 1 << 23; +const LAHF_SAHF: u32 = 1 << 0; + +const FEATURES_X86_64_V2: u32 = SSE3 | SSSE3 | CMPXCHG16B | SSE4_1 | SSE4_2 | POPCNT; +const EXTENDED_FEATURES_X86_64_V2: u32 = LAHF_SAHF; + +const UNSUPPORTED_CPU: &[u8] = b"This build of Stalwart requires an x86-64-v2 compatible CPU (SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF). If Stalwart runs in a virtual machine, set its CPU type to 'host' or 'x86-64-v2'.\n"; +const EXIT_UNSUPPORTED_CPU: i32 = 1; + +#[used] +#[unsafe(link_section = ".init_array.00001")] +static ASSERT_X86_64_V2: extern "C" fn() = assert_x86_64_v2; + +extern "C" fn assert_x86_64_v2() { + let features = __cpuid(LEAF_FEATURES).ecx; + let extended_features = if __cpuid(LEAF_EXTENDED_MAX).eax >= LEAF_EXTENDED_FEATURES { + __cpuid(LEAF_EXTENDED_FEATURES).ecx + } else { + 0 + }; + + if features & FEATURES_X86_64_V2 != FEATURES_X86_64_V2 + || extended_features & EXTENDED_FEATURES_X86_64_V2 != EXTENDED_FEATURES_X86_64_V2 + { + unsafe { + libc::write( + libc::STDERR_FILENO, + UNSUPPORTED_CPU.as_ptr().cast(), + UNSUPPORTED_CPU.len(), + ); + libc::_exit(EXIT_UNSUPPORTED_CPU); + } + } +} diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 2b628192..92f792ed 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -9,6 +9,8 @@ #![warn(clippy::cast_possible_wrap)] #![warn(clippy::cast_sign_loss)] +#[cfg(target_os = "linux")] +use common::manager::systemd::ServiceState; use common::{BuildServer, config::server::ServerProtocol, manager::boot::BootManager}; use http::HttpSessionManager; use imap::core::ImapSessionManager; @@ -20,6 +22,12 @@ use std::time::Duration; use trc::Collector; use utils::wait_for_shutdown; +#[cfg(all( + target_arch = "x86_64", + target_feature = "sse4.2", + any(target_os = "linux", target_os = "freebsd") +))] +mod cpu; #[cfg(feature = "dev_mode")] pub mod test_data; @@ -108,9 +116,15 @@ async fn main() -> std::io::Result<()> { // Start broadcast subscriber spawn_broadcast_subscriber(init.inner, shutdown_rx); + #[cfg(target_os = "linux")] + ServiceState::Ready.notify(); + // Wait for shutdown signal wait_for_shutdown().await; + #[cfg(target_os = "linux")] + ServiceState::Stopping.notify(); + // Shutdown collector Collector::shutdown(); diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index 9b02fd7f..90eafdf5 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managesieve" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index f711de36..db658149 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migration" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml index b8c95419..269ce4a3 100644 --- a/crates/nlp/Cargo.toml +++ b/crates/nlp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nlp" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index 211b87c0..59e41d8a 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pop3" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 263c2a12..fbecd43c 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "registry" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/scim-proto/Cargo.toml b/crates/scim-proto/Cargo.toml index bd822392..b9fb3719 100644 --- a/crates/scim-proto/Cargo.toml +++ b/crates/scim-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scim-proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/scim/Cargo.toml b/crates/scim/Cargo.toml index 01fcd2fd..c8032830 100644 --- a/crates/scim/Cargo.toml +++ b/crates/scim/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scim" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index 36fdbdee..75ce8d1f 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "services" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 0748c028..645e10ec 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/smtp/src/inbound/milter/client.rs b/crates/smtp/src/inbound/milter/client.rs index fb86cda5..c7133b90 100644 --- a/crates/smtp/src/inbound/milter/client.rs +++ b/crates/smtp/src/inbound/milter/client.rs @@ -238,8 +238,9 @@ impl MilterClient { } } } + } - // Write EndOfHeaders + if !self.has_option(SMFIP_NOEOH) { self.write(Command::EndOfHeader).await?; if !self.has_option(SMFIP_NR_EOH) { return self.read().await?.into_action(); diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index bfe0b210..aafb710a 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spam-filter" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 715e386a..a592a3f6 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "store" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] @@ -12,7 +12,7 @@ registry = { path = "../registry" } rocksdb = { version = "0.24", optional = true, features = ["multi-threaded-cf"] } foundationdb = { version = "0.11", features = ["embedded-fdb-include", "fdb-7_4"], optional = true } rusqlite = { version = "0.40", features = ["bundled"], optional = true } -rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"], optional = true } +rust-s3 = { version = "0.38", default-features = false, features = ["tokio-rustls-tls"], optional = true } reqwest_s3 = { package = "reqwest", version = "0.12", default-features = false, features = ["rustls-tls-native-roots"], optional = true } azure_core = { version = "0.21.0", optional = true } azure_storage = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true } diff --git a/crates/store/src/backend/rocksdb/main.rs b/crates/store/src/backend/rocksdb/main.rs index 39ddf245..1b5d1517 100644 --- a/crates/store/src/backend/rocksdb/main.rs +++ b/crates/store/src/backend/rocksdb/main.rs @@ -72,9 +72,8 @@ impl RocksDbStore { let mut cf_opts = cf_options(CfProfile::Blob, &cache, write_buffer_size); cf_opts.set_enable_blob_files(true); cf_opts.set_min_blob_size(config.blob_size); - cf_opts.set_enable_blob_gc(true); + cf_opts.set_enable_blob_gc(false); cf_opts.set_blob_gc_age_cutoff(1.0); - cf_opts.set_blob_gc_force_threshold(0.5); cfs.push(ColumnFamilyDescriptor::new(CF_BLOBS, cf_opts)); // Other cfs diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs index fbe77ade..3d8d2ac4 100644 --- a/crates/store/src/backend/rocksdb/write.rs +++ b/crates/store/src/backend/rocksdb/write.rs @@ -6,8 +6,8 @@ use super::{CF_INDEXES, CF_LOGS, CfHandle, RocksDbStore, into_error}; use crate::{ - Deserialize, IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, - SUBSPACE_QUOTA, + Deserialize, IndexKey, Key, LogKey, SUBSPACE_BLOBS, SUBSPACE_COUNTER, + SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_QUOTA, backend::deserialize_i64_le, write::{ AssignedIds, Batch, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult, Operation, @@ -16,8 +16,8 @@ use crate::{ }; use rand::RngExt; use rocksdb::{ - BoundColumnFamily, ErrorKind, IteratorMode, OptimisticTransactionDB, - OptimisticTransactionOptions, WriteOptions, + BottommostLevelCompaction, BoundColumnFamily, CompactOptions, ErrorKind, IteratorMode, + OptimisticTransactionDB, OptimisticTransactionOptions, WriteOptions, }; use std::{ sync::Arc, @@ -25,6 +25,9 @@ use std::{ time::{Duration, Instant}, }; +const BLOB_GC_MIN_GARBAGE: u64 = 256 * 1024 * 1024; +const BLOB_GC_GARBAGE_RATIO: f64 = 0.25; + impl RocksDbStore { pub(crate) async fn write(&self, mut batch: Batch<'_>) -> trc::Result { let db = self.db.clone(); @@ -114,6 +117,28 @@ impl RocksDbStore { } } + let cf = db.subspace_handle(SUBSPACE_BLOBS); + let total = db + .property_int_value_cf(&cf, "rocksdb.live-blob-file-size") + .map_err(into_error)? + .unwrap_or_default(); + let garbage = db + .property_int_value_cf(&cf, "rocksdb.live-blob-file-garbage-size") + .map_err(into_error)? + .unwrap_or_default(); + + if garbage >= BLOB_GC_MIN_GARBAGE + && garbage as f64 >= total as f64 * BLOB_GC_GARBAGE_RATIO + { + db.set_options_cf(&cf, &[("enable_blob_garbage_collection", "true")]) + .map_err(into_error)?; + let mut opts = CompactOptions::default(); + opts.set_bottommost_level_compaction(BottommostLevelCompaction::Force); + db.compact_range_cf_opt(&cf, None::<&[u8]>, None::<&[u8]>, &opts); + db.set_options_cf(&cf, &[("enable_blob_garbage_collection", "false")]) + .map_err(into_error)?; + } + Ok(()) }) .await diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index 68dfd0a2..5049902a 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trc" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/trc/event-macro/Cargo.toml b/crates/trc/event-macro/Cargo.toml index 094ad47c..9d62c9d8 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.25" +version = "0.16.26" edition = "2024" [lib] diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 70ce4493..5fb53204 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "types" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index b9fd9331..09c235f5 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/utils/proc-macros/Cargo.toml b/crates/utils/proc-macros/Cargo.toml index 85ae8e42..3391f73e 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.25" +version = "0.16.26" edition = "2024" [lib] diff --git a/docker-bake.hcl b/docker-bake.hcl index ec490a35..2cc42fe5 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -17,7 +17,8 @@ target "docker-metadata-action" {} target "build" { secret = [ "type=env,id=ACTIONS_RESULTS_URL", - "type=env,id=ACTIONS_RUNTIME_TOKEN" + "type=env,id=ACTIONS_RUNTIME_TOKEN", + "type=env,id=GITHUB_TOKEN" ] args = { TARGET = "${TARGET}" diff --git a/resources/systemd/stalwart-mail.service b/resources/systemd/stalwart-mail.service index 4e2cb463..b45b0357 100644 --- a/resources/systemd/stalwart-mail.service +++ b/resources/systemd/stalwart-mail.service @@ -5,7 +5,7 @@ ConditionPathExists=__PATH__/etc/config.json After=network-online.target [Service] -Type=simple +Type=notify LimitNOFILE=65536 KillMode=process KillSignal=SIGINT diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 1d66726e..9de68d87 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.16.25" +version = "0.16.26" edition = "2024" [features] @@ -83,7 +83,7 @@ rcgen = "0.14" sha2 = "0.11" time = "0.3" testcontainers = { version = "0.28", features = ["reusable-containers"] } -rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"] } +rust-s3 = { version = "0.38", default-features = false, features = ["tokio-rustls-tls"] } [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] tikv-jemallocator = "0.7" diff --git a/tests/docker/keycloak/stalwart-realm.json b/tests/docker/keycloak/stalwart-realm.json index e61a46bb..43b051b0 100644 --- a/tests/docker/keycloak/stalwart-realm.json +++ b/tests/docker/keycloak/stalwart-realm.json @@ -133,6 +133,72 @@ } } ] + }, + { + "clientId": "stalwart-userinfo-username", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "stalwart-userinfo-username-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "publicClient": false, + "protocol": "openid-connect", + "directAccessGrantsEnabled": true, + "standardFlowEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "openid" + ], + "optionalClientScopes": [ + "email", + "profile", + "roles" + ], + "protocolMappers": [ + { + "name": "email-claim", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "userinfo.token.claim": "true", + "jsonType.label": "String" + } + }, + { + "name": "login-userinfo-only", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "login", + "claim.value": "jdoe", + "id.token.claim": "false", + "access.token.claim": "false", + "userinfo.token.claim": "true", + "jsonType.label": "String" + } + }, + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "stalwart", + "id.token.claim": "false", + "access.token.claim": "true" + } + } + ] } ], "users": [ diff --git a/tests/src/jmap/mail/mailbox.rs b/tests/src/jmap/mail/mailbox.rs index 81e5fa71..32210b9f 100644 --- a/tests/src/jmap/mail/mailbox.rs +++ b/tests/src/jmap/mail/mailbox.rs @@ -229,6 +229,37 @@ pub async fn test(test: &TestServer) { "{result:?}", ); + // Moving a mailbox under a parent that has a sibling with the same name fails + let nested_id = client + .mailbox_create("level 2", Some(&id_map["l.1"]), Role::None) + .await + .unwrap() + .take_id(); + let result = client + .mailbox_move(&nested_id, Some(&id_map["inbox"])) + .await; + assert!( + matches!( + result, + Err(Error::Set(SetError { + type_: SetErrorType::AlreadyExists, + .. + })) + ), + "{result:?}", + ); + client.mailbox_destroy(&nested_id, true).await.unwrap(); + + // Changing only the case of a mailbox name succeeds + client + .mailbox_rename(&id_map["l.2"], "LEVEL 2") + .await + .unwrap(); + client + .mailbox_rename(&id_map["l.2"], "Level 2") + .await + .unwrap(); + // Circular relationship let mut request = client.build(); request diff --git a/tests/src/jmap/mail/set.rs b/tests/src/jmap/mail/set.rs index 660bc195..0080382e 100644 --- a/tests/src/jmap/mail/set.rs +++ b/tests/src/jmap/mail/set.rs @@ -363,7 +363,10 @@ async fn update_preserves_uids( assert_eq!(uids[&INBOX_ID], inbox_uid); assert_ne!(uids[&test_mailbox_document_id], 0); - client.mailbox_destroy(&test_mailbox_id, true).await.unwrap(); + client + .mailbox_destroy(&test_mailbox_id, true) + .await + .unwrap(); } async fn message_uids( diff --git a/tests/src/jmap/mail/submission.rs b/tests/src/jmap/mail/submission.rs index 110390a5..52827cd7 100644 --- a/tests/src/jmap/mail/submission.rs +++ b/tests/src/jmap/mail/submission.rs @@ -510,6 +510,117 @@ pub async fn test(test: &TestServer) { .unwrap() .is_none() ); + + // onSuccess actions keyed by an existing submission id act on its own email + let mut held = Vec::with_capacity(2); + for subject in ["held 1", "held 2"] { + let draft_id = client + .email_import( + format!("From: jdoe@example.com\r\nTo: jane_smith@remote.org\r\nSubject: {subject}\r\n\r\ntest") + .into_bytes(), + [&mailbox_id], + None::>, + None, + ) + .await + .unwrap() + .take_id(); + let submission_id = client + .email_submission_create_envelope( + &draft_id, + &identity_id, + Address::new("jdoe@example.com") + .parameter("HOLDUNTIL", Some(hold_until_date.to_string())), + ["jane_smith@remote.org"], + ) + .await + .unwrap() + .take_id(); + held.push((submission_id, draft_id)); + } + let [(submission_id_1, draft_id_1), (submission_id_2, draft_id_2)] = + <[_; 2]>::try_from(held).unwrap(); + let mut found_filler = false; + for _ in 0..64 { + if client + .email_get(&submission_id_1, None::>) + .await + .unwrap() + .is_some() + { + found_filler = true; + break; + } + client + .email_import( + b"From: jdoe@example.com\r\nSubject: filler\r\n\r\ntest".to_vec(), + [&mailbox_id_2], + None::>, + None, + ) + .await + .unwrap(); + } + assert!(found_filler, "No email shares the submission document id"); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; + + let mut request = client.build(); + let set_request = request.set_email_submission(); + set_request + .update(&submission_id_1) + .undo_status(UndoStatus::Canceled); + set_request + .arguments() + .on_success_destroy_email_id(&submission_id_1); + request.send().await.unwrap().unwrap_method_responses(); + assert!( + client + .email_get(&draft_id_1, None::>) + .await + .unwrap() + .is_none() + ); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; + + // onSuccessUpdateEmail is skipped for a submission whose update failed + let mut request = client.build(); + let set_request = request.set_email_submission(); + set_request + .update(&submission_id_1) + .undo_status(UndoStatus::Canceled); + set_request + .update(&submission_id_2) + .undo_status(UndoStatus::Canceled); + set_request + .arguments() + .on_success_update_email_id(&submission_id_1) + .keyword("$draft", true); + set_request + .arguments() + .on_success_update_email_id(&submission_id_2) + .keyword("$draft", true) + .mailbox_id(&mailbox_id, false) + .mailbox_id(&mailbox_id_2, true); + request.send().await.unwrap().unwrap_method_responses(); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; + assert_email_properties(&client, &draft_id_2, &[&mailbox_id_2], &["$draft"]).await; + + // onSuccessDestroyEmail keyed by a destroyed submission id + let mut request = client.build(); + let set_request = request.set_email_submission(); + set_request.destroy([&submission_id_2]); + set_request + .arguments() + .on_success_destroy_email_id(&submission_id_2); + request.send().await.unwrap().unwrap_method_responses(); + assert!( + client + .email_get(&draft_id_2, None::>) + .await + .unwrap() + .is_none() + ); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; smtp_settings.lock().do_stop = true; // Identities are created and destroyed as the account's addresses change diff --git a/tests/src/jmap/mail/vacation_response.rs b/tests/src/jmap/mail/vacation_response.rs index 0eb06502..d200f998 100644 --- a/tests/src/jmap/mail/vacation_response.rs +++ b/tests/src/jmap/mail/vacation_response.rs @@ -11,6 +11,7 @@ use crate::{ utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection}, }; use chrono::{TimeDelta, Utc}; +use jmap_client::core::response::VacationResponseSetResponse; use std::time::Instant; pub async fn test(test: &TestServer) { @@ -29,6 +30,42 @@ pub async fn test(test: &TestServer) { Instant::now() + std::time::Duration::from_secs(10), ); + // Enabling with only isEnabled on a fresh account uses the default response + let mut request = client.build(); + request + .set_vacation_response() + .update("singleton") + .is_enabled(true); + request + .send_single::() + .await + .unwrap() + .updated("singleton") + .unwrap(); + let mut lmtp = SmtpConnection::connect().await; + lmtp.ingest( + "michael@remote.org", + &["jdoe@example.com"], + concat!( + "From: michael@remote.org\r\n", + "To: jdoe@example.com\r\n", + "Subject: Printer\r\n", + "\r\n", + "PC load letter? What does that even mean?", + ), + ) + .await; + lmtp.quit().await; + assert_message_delivery( + &mut smtp_rx, + MockMessage::new( + "", + [""], + "@I am away.", + ), + ) + .await; + // Let people know that we'll be down in Kokomo client .vacation_response_enable( -- 2.54.0 From 8a4f1febe58741853db1e61382276af86df90e19 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 12 Oct 2026 07:39:24 -0700 Subject: [PATCH 2/2] Check the hickory upgrade test by its outcome The vendored hickory proves the delegation below google.com insecure itself, so validated_lookup no longer takes the recheck path for aspmx.l.google.com and the test's `validated.insecure` assertion failed. Assert what matters instead: the answer comes out insecure, by either path. Through Hetzner's resolver, hickory alone also accepts the signed-CNAME TLSA denial now; the recheck stays as a fallback. --- crates/smtp/src/outbound/dane/dnssec.rs | 24 +++++++++++++++++------- vendor/hickory-dns/VENDORED.md | 9 ++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/smtp/src/outbound/dane/dnssec.rs b/crates/smtp/src/outbound/dane/dnssec.rs index bc136624..f71189c3 100644 --- a/crates/smtp/src/outbound/dane/dnssec.rs +++ b/crates/smtp/src/outbound/dane/dnssec.rs @@ -436,7 +436,9 @@ impl TlsaLookup for Server { } // inbuxa: hickory 0.26.3 calls some valid answers bogus, and the queue then -// retries those hosts until the message expires. Two cases seen in production: +// retries those hosts until the message expires. Two cases seen in production +// (the hickory vendored since upstream v0.16.26 gets both right in the +// `--ignored` tests below; this stays as the fallback): // // - A zone delegated beneath an unsigned zone, such as `l.google.com` under // `google.com`. To prove the delegation insecure, hickory wants an SOA @@ -844,7 +846,10 @@ mod tests { } // Needs the network: a signed MX pointing into a zone delegated beneath an - // unsigned one. Run with `--ignored` to check a hickory upgrade. + // unsigned one. Run with `--ignored` to check a hickory upgrade. hickory + // 0.26.3 called it bogus and the recheck proved it insecure; the vendored + // hickory proves it itself. Either way the answer has to come out insecure. + // The TLSA test below prints whether hickory alone gets its case right. #[tokio::test] #[ignore] async fn validated_lookup_proves_delegation_below_unsigned_zone() { @@ -870,12 +875,17 @@ mod tests { }; let (dnssec, plain) = (build(true), build(false)); - let validated = - validated_lookup(&dnssec, &plain, name("aspmx.l.google.com."), RecordType::A) - .await - .unwrap(); - assert!(validated.insecure); + let query = name("aspmx.l.google.com."); + let validated = validated_lookup(&dnssec, &plain, query.clone(), RecordType::A) + .await + .unwrap(); assert!(!validated.lookup.answers().is_empty()); + let status = if validated.insecure { + DnssecStatus::Insecure + } else { + tlsa_base_status(&query, validated.lookup.answers(), RecordType::A) + }; + assert_eq!(status, DnssecStatus::Insecure); } // Needs the network: a TLSA name that is a signed CNAME to the zone apex, diff --git a/vendor/hickory-dns/VENDORED.md b/vendor/hickory-dns/VENDORED.md index e8ba6955..9c3cd1da 100644 --- a/vendor/hickory-dns/VENDORED.md +++ b/vendor/hickory-dns/VENDORED.md @@ -23,9 +23,12 @@ manifest normalized, so it builds outside hickory's workspace), without `Cargo.lock` and `Cargo.toml.orig`. `.cargo_vcs_info.json` records the commit. -The signed-CNAME-to-NODATA validation bug is not fixed on that branch, so -`validated_lookup` in `crates/smtp/src/outbound/dane/dnssec.rs` still -rechecks bogus answers. +Both answers hickory 0.26.3 called bogus in production (an insecure +delegation below an unsigned zone, and a signed CNAME to a name without the +type asked for) validate with this copy: the `--ignored` tests at the end of +`crates/smtp/src/outbound/dane/dnssec.rs` show it, the TLSA one through +Hetzner's resolver (2026-10-12). `validated_lookup` there still rechecks +answers hickory calls bogus, as a fallback. ## Updating -- 2.54.0