about summary refs log tree commit diff stats
path: root/pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch')
-rw-r--r--pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch747
1 files changed, 0 insertions, 747 deletions
diff --git a/pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch b/pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch
deleted file mode 100644
index 392fbde..0000000
--- a/pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch
+++ /dev/null
@@ -1,747 +0,0 @@
-From 66227b07c6cb4781a38fe603c2e856c5696e0f94 Mon Sep 17 00:00:00 2001
-From: aszlig <aszlig@nix.build>
-Date: Wed, 21 May 2025 19:03:55 +0200
-Subject: [PATCH] Allow to switch to operating system CA root store
-
-So far, we only used the root CA certificates from the "webpki-roots"
-crate, which makes it very difficult if you want to run a custom CA.
-
-In my case I'm running automated tests of full production systems, which
-also includes an ACME setup that injects a Pebble instance into the
-system's CA certificates.
-
-Since Stalwart doesn't use the system's root CA certificate store, it's
-very difficult to inject the certificate for the Pebble instance.
-
-Given that there's also some interest (issue #247) for doing this in an
-enterprise environment with intranet CAs, I decided to generalise this
-far enough that it can not only be used in my downstream setup.
-
-Currently, this doesn't fully address the issue, since in the long term
-this might be something we'd want to configure at runtime, as per
-@mdecimus's comment[1]:
-
-> A slightly more complex approach is required that allows the user to
-> select at runtime which CA store to use.
-
-However, this makes it at least easier to switch to native root CA store
-by simply recompiling with the "tls-native-roots" feature.
-
-[1]: https://github.com/stalwartlabs/stalwart/issues/247#issuecomment-2437039500
-
-Signed-off-by: aszlig <aszlig@nix.build>
-Issue: https://github.com/stalwartlabs/stalwart/issues/247
----
- Cargo.lock                                   |  4 ++
- crates/cli/Cargo.toml                        | 10 +++--
- crates/cli/src/main.rs                       |  8 ++--
- crates/common/Cargo.toml                     |  2 +-
- crates/common/src/enterprise/llm.rs          |  2 +-
- crates/common/src/enterprise/mod.rs          | 18 +++++---
- crates/common/src/lib.rs                     |  3 +-
- crates/common/src/listener/acme/directory.rs |  6 +--
- crates/common/src/manager/mod.rs             |  2 +-
- crates/common/src/scripts/plugins/http.rs    |  2 +-
- crates/common/src/telemetry/webhooks/mod.rs  |  2 +-
- crates/directory/Cargo.toml                  |  2 +-
- crates/directory/src/backend/oidc/lookup.rs  |  4 +-
- crates/jmap/Cargo.toml                       |  2 +-
- crates/main/Cargo.toml                       |  1 +
- crates/services/Cargo.toml                   |  2 +-
- crates/services/src/state_manager/http.rs    |  2 +-
- crates/smtp/Cargo.toml                       |  2 +-
- crates/smtp/src/inbound/hooks/client.rs      |  2 +-
- crates/smtp/src/outbound/mta_sts/lookup.rs   |  3 +-
- crates/smtp/src/reporting/tls.rs             |  5 +--
- crates/spam-filter/Cargo.toml                |  2 +-
- crates/spam-filter/src/analysis/url.rs       |  2 +-
- crates/store/Cargo.toml                      |  2 +-
- crates/store/src/backend/azure/mod.rs        |  2 +-
- crates/store/src/backend/http/lookup.rs      |  2 +-
- crates/trc/Cargo.toml                        |  2 +-
- crates/utils/Cargo.toml                      |  2 +
- crates/utils/src/lib.rs                      | 46 ++++++++++++++------
- crates/utils/src/suffixlist.rs               |  7 ++-
- tests/Cargo.toml                             |  2 +-
- tests/src/jmap/auth_oauth.rs                 |  6 +--
- tests/src/jmap/mod.rs                        |  4 +-
- tests/src/smtp/management/queue.rs           |  2 +-
- tests/src/webdav/mod.rs                      |  2 +-
- 35 files changed, 103 insertions(+), 64 deletions(-)
-
-diff --git a/Cargo.lock b/Cargo.lock
-index 0eeb42510..6dd394dc3 100644
---- a/Cargo.lock
-+++ b/Cargo.lock
-@@ -3372,6 +3372,7 @@ dependencies = [
-  "hyper 1.6.0",
-  "hyper-util",
-  "rustls 0.23.27",
-+ "rustls-native-certs 0.8.1",
-  "rustls-pki-types",
-  "tokio",
-  "tokio-rustls 0.26.2",
-@@ -6318,6 +6319,7 @@ dependencies = [
-  "pin-project-lite",
-  "quinn",
-  "rustls 0.23.27",
-+ "rustls-native-certs 0.8.1",
-  "rustls-pemfile 2.2.0",
-  "rustls-pki-types",
-  "serde",
-@@ -7694,6 +7696,7 @@ dependencies = [
-  "serde",
-  "serde_json",
-  "tokio",
-+ "utils",
- ]
- 
- [[package]]
-@@ -8795,6 +8798,7 @@ dependencies = [
-  "rustls 0.23.27",
-  "rustls-pemfile 2.2.0",
-  "rustls-pki-types",
-+ "rustls-platform-verifier",
-  "serde",
-  "serde_json",
-  "smtp-proto",
-diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
-index 5573df819..719441a0c 100644
---- a/crates/cli/Cargo.toml
-+++ b/crates/cli/Cargo.toml
-@@ -11,9 +11,9 @@ readme = "README.md"
- resolver = "2"
- 
- [dependencies]
--jmap-client = { version = "0.3", features = ["async"] } 
--mail-parser = { version = "0.11", features = ["full_encoding", "serde"] } 
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
-+jmap-client = { version = "0.3", features = ["async"] }
-+mail-parser = { version = "0.11", features = ["full_encoding", "serde"] }
-+reqwest = { version = "0.12", default-features = false, features = ["http2"]}
- tokio = { version = "1.45", features = ["full"] }
- num_cpus = "1.13.1"
- clap = { version = "4.1.6", features = ["derive"] }
-@@ -30,3 +30,7 @@ futures = "0.3.28"
- pwhash = "1.0.0"
- rand = "0.9.0"
- mail-auth = { version = "0.7" }
-+utils.path = "../utils"
-+
-+[features]
-+tls-native-roots = ["utils/tls-native-roots"]
-diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
-index f295c217f..f62d528c5 100644
---- a/crates/cli/src/main.rs
-+++ b/crates/cli/src/main.rs
-@@ -86,7 +86,7 @@ fn parse_credentials(credentials: &str) -> Credentials {
- 
- async fn oauth(url: &str) -> Credentials {
-     let metadata: HashMap<String, serde_json::Value> = serde_json::from_slice(
--        &reqwest::Client::builder()
-+        &utils::reqwest_client_builder()
-             .danger_accept_invalid_certs(is_localhost(url))
-             .build()
-             .unwrap_or_default()
-@@ -104,7 +104,7 @@ async fn oauth(url: &str) -> Credentials {
-     let mut params: HashMap<String, String> =
-         HashMap::from_iter([("client_id".to_string(), "Stalwart_CLI".to_string())]);
-     let response: HashMap<String, serde_json::Value> = serde_json::from_slice(
--        &reqwest::Client::builder()
-+        &utils::reqwest_client_builder()
-             .danger_accept_invalid_certs(is_localhost(url))
-             .build()
-             .unwrap_or_default()
-@@ -138,7 +138,7 @@ async fn oauth(url: &str) -> Credentials {
-     std::io::stdin().lock().lines().next();
- 
-     let mut response: HashMap<String, serde_json::Value> = serde_json::from_slice(
--        &reqwest::Client::builder()
-+        &utils::reqwest_client_builder()
-             .danger_accept_invalid_certs(is_localhost(url))
-             .build()
-             .unwrap_or_default()
-@@ -230,7 +230,7 @@ impl Client {
-             },
-             url
-         );
--        let mut request = reqwest::Client::builder()
-+        let mut request = utils::reqwest_client_builder()
-             .danger_accept_invalid_certs(is_localhost(&url))
-             .timeout(Duration::from_secs(self.timeout.unwrap_or(60)))
-             .build()
-diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml
-index 58028bad3..5add0a5dc 100644
---- a/crates/common/Cargo.toml
-+++ b/crates/common/Cargo.toml
-@@ -33,7 +33,7 @@ tokio = { version = "1.45", features = ["net", "macros"] }
- tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
- futures = "0.3"
- rcgen = "0.12"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
-+reqwest = { version = "0.12", default-features = false, features = ["http2", "stream"]}
- serde = { version = "1.0", features = ["derive"]}
- serde_json = "1.0"
- base64 = "0.22"
-diff --git a/crates/common/src/enterprise/llm.rs b/crates/common/src/enterprise/llm.rs
-index 8338f39d3..fe013c0f0 100644
---- a/crates/common/src/enterprise/llm.rs
-+++ b/crates/common/src/enterprise/llm.rs
-@@ -125,7 +125,7 @@ impl AiApiConfig {
-         };
- 
-         // Send request
--        let response = reqwest::Client::builder()
-+        let response = utils::reqwest_client_builder()
-             .timeout(self.timeout)
-             .danger_accept_invalid_certs(self.tls_allow_invalid_certs)
-             .build()
-diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs
-index 9fc8de495..ddaf27880 100644
---- a/crates/common/src/enterprise/mod.rs
-+++ b/crates/common/src/enterprise/mod.rs
-@@ -188,12 +188,18 @@ impl Server {
- 
-                 let mut logo = None;
-                 if let Some(logo_url) = logo_url {
--                    let response = reqwest::get(logo_url.as_str()).await.map_err(|err| {
--                        trc::ResourceEvent::DownloadExternal
--                            .into_err()
--                            .details("Failed to download logo")
--                            .reason(err)
--                    })?;
-+                    let response = utils::reqwest_client_builder()
-+                        .build()
-+                        .unwrap_or_default()
-+                        .get(logo_url.as_str())
-+                        .send()
-+                        .await
-+                        .map_err(|err| {
-+                            trc::ResourceEvent::DownloadExternal
-+                                .into_err()
-+                                .details("Failed to download logo")
-+                                .reason(err)
-+                        })?;
- 
-                     let content_type = response
-                         .headers()
-diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs
-index ce4b41b89..185508d1f 100644
---- a/crates/common/src/lib.rs
-+++ b/crates/common/src/lib.rs
-@@ -46,6 +46,8 @@ use utils::{
-     snowflake::SnowflakeIdGenerator,
- };
- 
-+pub use utils::USER_AGENT;
-+
- pub mod addresses;
- pub mod auth;
- pub mod config;
-@@ -67,7 +69,6 @@ pub use psl;
- pub static VERSION_PRIVATE: &str = env!("CARGO_PKG_VERSION");
- pub static VERSION_PUBLIC: &str = "1.0.0";
- 
--pub static USER_AGENT: &str = "Stalwart/1.0.0";
- pub static DAEMON_NAME: &str = concat!("Stalwart v", env!("CARGO_PKG_VERSION"),);
- pub static PROD_ID: &str = "-//Stalwart Labs Ltd.//Stalwart Server//EN";
- 
-diff --git a/crates/common/src/listener/acme/directory.rs b/crates/common/src/listener/acme/directory.rs
-index f095e1969..6f9cfa0e0 100644
---- a/crates/common/src/listener/acme/directory.rs
-+++ b/crates/common/src/listener/acme/directory.rs
-@@ -7,7 +7,6 @@ use super::jose::{
- };
- use base64::Engine;
- use base64::engine::general_purpose::URL_SAFE_NO_PAD;
--use hyper::header::USER_AGENT;
- use rcgen::{Certificate, CustomExtension, PKCS_ECDSA_P256_SHA256};
- use reqwest::header::CONTENT_TYPE;
- use reqwest::{Method, Response};
-@@ -316,7 +315,7 @@ async fn https(
-     body: Option<String>,
- ) -> trc::Result<Response> {
-     let url = url.as_ref();
--    let mut builder = reqwest::Client::builder()
-+    let mut builder = utils::reqwest_client_builder()
-         .timeout(Duration::from_secs(30))
-         .http1_only();
- 
-@@ -330,8 +329,7 @@ async fn https(
-     let mut request = builder
-         .build()
-         .map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_http_error(err))?
--        .request(method, url)
--        .header(USER_AGENT, crate::USER_AGENT);
-+        .request(method, url);
- 
-     if let Some(body) = body {
-         request = request
-diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs
-index 51861a82c..a59e74b56 100644
---- a/crates/common/src/manager/mod.rs
-+++ b/crates/common/src/manager/mod.rs
-@@ -76,7 +76,7 @@ pub async fn fetch_resource(
-             .await
-             .map_err(|err| format!("Failed to read {path}: {err}"))
-     } else {
--        let response = reqwest::Client::builder()
-+        let response = utils::reqwest_client_builder()
-             .timeout(timeout)
-             .danger_accept_invalid_certs(is_localhost_url(url))
-             .user_agent(USER_AGENT)
-diff --git a/crates/common/src/scripts/plugins/http.rs b/crates/common/src/scripts/plugins/http.rs
-index 42e2af553..54d906e17 100644
---- a/crates/common/src/scripts/plugins/http.rs
-+++ b/crates/common/src/scripts/plugins/http.rs
-@@ -26,7 +26,7 @@ pub async fn exec_header(ctx: PluginContext<'_>) -> trc::Result<Variable> {
-         return Ok(Variable::from(url.split_once("/?").unwrap().1.to_string()));
-     }
- 
--    reqwest::Client::builder()
-+    utils::reqwest_client_builder()
-         .user_agent(agent.as_ref())
-         .timeout(Duration::from_millis(timeout))
-         .redirect(Policy::none())
-diff --git a/crates/common/src/telemetry/webhooks/mod.rs b/crates/common/src/telemetry/webhooks/mod.rs
-index a70005399..2cebfd81f 100644
---- a/crates/common/src/telemetry/webhooks/mod.rs
-+++ b/crates/common/src/telemetry/webhooks/mod.rs
-@@ -148,7 +148,7 @@ async fn post_webhook_events(
-     }
- 
-     // Send request
--    let response = reqwest::Client::builder()
-+    let response = utils::reqwest_client_builder()
-         .timeout(settings.timeout)
-         .danger_accept_invalid_certs(settings.tls_allow_invalid_certs)
-         .build()
-diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml
-index ae8eca025..4a033834b 100644
---- a/crates/directory/Cargo.toml
-+++ b/crates/directory/Cargo.toml
-@@ -35,7 +35,7 @@ futures = "0.3"
- regex = "1.7.0"
- serde = { version = "1.0", features = ["derive"]}
- totp-rs = { version = "5.5.1", features = ["otpauth"] }
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] }
-+reqwest = { version = "0.12", default-features = false, features = ["http2"] }
- serde_json = "1.0"
- base64 = "0.22"
- rkyv = { version = "0.8.10", features = ["little_endian"] }
-diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs
-index 755064115..907831a46 100644
---- a/crates/directory/src/backend/oidc/lookup.rs
-+++ b/crates/directory/src/backend/oidc/lookup.rs
-@@ -36,10 +36,10 @@ impl OpenIdDirectory {
-             QueryBy::Credentials(Credentials::OAuthBearer { token }) => {
-                 // Send request
-                 #[cfg(feature = "test_mode")]
--                let client = reqwest::Client::builder().danger_accept_invalid_certs(true);
-+                let client = utils::reqwest_client_builder().danger_accept_invalid_certs(true);
- 
-                 #[cfg(not(feature = "test_mode"))]
--                let client = reqwest::Client::builder();
-+                let client = utils::reqwest_client_builder();
- 
-                 let client = client
-                     .timeout(self.config.endpoint_timeout)
-diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml
-index 9d9cfa7d7..a3a7b5003 100644
---- a/crates/jmap/Cargo.toml
-+++ b/crates/jmap/Cargo.toml
-@@ -36,7 +36,7 @@ p256 = { version = "0.13", features = ["ecdh"] }
- hkdf = "0.12.3"
- sha1 = "0.10"
- sha2 = "0.10"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
-+reqwest = { version = "0.12", default-features = false, features = ["http2"]}
- tokio-tungstenite = "0.26"
- tungstenite = "0.26"
- chrono = "0.4"
-diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml
-index 1023b73bf..4b39dae29 100644
---- a/crates/main/Cargo.toml
-+++ b/crates/main/Cargo.toml
-@@ -64,3 +64,4 @@ enterprise = [ "jmap/enterprise",
-                "dav/enterprise",
-                "groupware/enterprise",
-                "services/enterprise" ]
-+tls-native-roots = ["utils/tls-native-roots"]
-diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml
-index 11bb76e5a..35aa0b6cb 100644
---- a/crates/services/Cargo.toml
-+++ b/crates/services/Cargo.toml
-@@ -24,7 +24,7 @@ rsa = "0.9.2"
- p256 = { version = "0.13", features = ["ecdh"] }
- hkdf = "0.12.3"
- sha2 = "0.10"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
-+reqwest = { version = "0.12", default-features = false, features = ["http2"]}
- base64 = "0.22"
- compact_str = "0.9.0"
- 
-diff --git a/crates/services/src/state_manager/http.rs b/crates/services/src/state_manager/http.rs
-index edd01865f..ee26b8482 100644
---- a/crates/services/src/state_manager/http.rs
-+++ b/crates/services/src/state_manager/http.rs
-@@ -63,7 +63,7 @@ pub(crate) async fn http_request(
-     keys: Option<EncryptionKeys>,
-     push_timeout: Duration,
- ) -> bool {
--    let client_builder = reqwest::Client::builder().timeout(push_timeout);
-+    let client_builder = utils::reqwest_client_builder().timeout(push_timeout);
- 
-     #[cfg(feature = "test_mode")]
-     let client_builder = client_builder.danger_accept_invalid_certs(true);
-diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml
-index e4a781796..d21665c92 100644
---- a/crates/smtp/Cargo.toml
-+++ b/crates/smtp/Cargo.toml
-@@ -47,7 +47,7 @@ blake3 = "1.3"
- lru-cache = "0.1.2"
- rand = "0.9.0"
- x509-parser = "0.17.0"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] }
-+reqwest = { version = "0.12", default-features = false, features = ["http2"] }
- serde = { version = "1.0", features = ["derive", "rc"] }
- serde_json = "1.0"
- num_cpus = "1.15.0"
-diff --git a/crates/smtp/src/inbound/hooks/client.rs b/crates/smtp/src/inbound/hooks/client.rs
-index 2e3a9e17c..6bfb40a63 100644
---- a/crates/smtp/src/inbound/hooks/client.rs
-+++ b/crates/smtp/src/inbound/hooks/client.rs
-@@ -13,7 +13,7 @@ pub(super) async fn send_mta_hook_request(
-     mta_hook: &MTAHook,
-     request: Request,
- ) -> Result<Response, String> {
--    let response = reqwest::Client::builder()
-+    let response = utils::reqwest_client_builder()
-         .timeout(mta_hook.timeout)
-         .danger_accept_invalid_certs(mta_hook.tls_allow_invalid_certs)
-         .build()
-diff --git a/crates/smtp/src/outbound/mta_sts/lookup.rs b/crates/smtp/src/outbound/mta_sts/lookup.rs
-index c8b279c2f..10f2aafeb 100644
---- a/crates/smtp/src/outbound/mta_sts/lookup.rs
-+++ b/crates/smtp/src/outbound/mta_sts/lookup.rs
-@@ -67,8 +67,7 @@ impl MtaStsLookup for Server {
- 
-         // Fetch policy
-         #[cfg(not(feature = "test_mode"))]
--        let bytes = reqwest::Client::builder()
--            .user_agent(common::USER_AGENT)
-+        let bytes = utils::reqwest_client_builder()
-             .timeout(timeout)
-             .redirect(reqwest::redirect::Policy::none())
-             .build()?
-diff --git a/crates/smtp/src/reporting/tls.rs b/crates/smtp/src/reporting/tls.rs
-index 28e9fa47b..875f395c8 100644
---- a/crates/smtp/src/reporting/tls.rs
-+++ b/crates/smtp/src/reporting/tls.rs
-@@ -8,7 +8,7 @@ use super::{AggregateTimestamp, SerializedSize};
- use crate::{queue::RecipientDomain, reporting::SmtpReporting};
- use ahash::AHashMap;
- use common::{
--    Server, USER_AGENT,
-+    Server,
-     config::smtp::{
-         report::AggregateFrequency,
-         resolver::{Mode, MxPattern},
-@@ -142,8 +142,7 @@ impl TlsReporting for Server {
-         for uri in &rua {
-             match uri {
-                 ReportUri::Http(uri) => {
--                    if let Ok(client) = reqwest::Client::builder()
--                        .user_agent(USER_AGENT)
-+                    if let Ok(client) = utils::reqwest_client_builder()
-                         .timeout(Duration::from_secs(2 * 60))
-                         .build()
-                     {
-diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml
-index f6ec739a8..5a3df8453 100644
---- a/crates/spam-filter/Cargo.toml
-+++ b/crates/spam-filter/Cargo.toml
-@@ -19,7 +19,7 @@ tokio = { version = "1.45", features = ["net", "macros"] }
- psl = "2"
- hyper = { version = "1.0.1", features = ["server", "http1", "http2"] }
- idna = "1.0"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
-+reqwest = { version = "0.12", default-features = false, features = ["http2", "stream"]}
- decancer = "3.0.1"
- unicode-security = "0.1.0"
- infer = "0.19"
-diff --git a/crates/spam-filter/src/analysis/url.rs b/crates/spam-filter/src/analysis/url.rs
-index a0d663917..e6377f13c 100644
---- a/crates/spam-filter/src/analysis/url.rs
-+++ b/crates/spam-filter/src/analysis/url.rs
-@@ -290,7 +290,7 @@ async fn http_get_header(
-             Ok(None)
-         };
-     }
--    reqwest::Client::builder()
-+    utils::reqwest_client_builder()
-         .user_agent("Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/118.0")
-         .timeout(timeout)
-         .redirect(Policy::none())
-diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml
-index 1f8965049..3bd1a228c 100644
---- a/crates/store/Cargo.toml
-+++ b/crates/store/Cargo.toml
-@@ -16,7 +16,7 @@ async-nats = { version = "0.40", default-features = false, features = ["server_2
- 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 }
- azure_storage_blobs = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true }
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
-+reqwest = { version = "0.12", default-features = false, features = ["http2", "stream"]}
- tokio = { version = "1.45", features = ["sync", "fs", "io-util"] }
- r2d2 = { version = "0.8.10", optional = true }
- futures = { version = "0.3", optional = true }
-diff --git a/crates/store/src/backend/azure/mod.rs b/crates/store/src/backend/azure/mod.rs
-index 8bbaea073..fbdb03eb7 100644
---- a/crates/store/src/backend/azure/mod.rs
-+++ b/crates/store/src/backend/azure/mod.rs
-@@ -63,7 +63,7 @@ impl AzureStore {
-         let timeout = config
-             .property_or_default::<Duration>((&prefix, "timeout"), "30s")
-             .unwrap_or_else(|| Duration::from_secs(30));
--        let transport = match reqwest::Client::builder().timeout(timeout).build() {
-+        let transport = match utils::reqwest_client_builder().timeout(timeout).build() {
-             Ok(client) => Arc::new(client),
-             Err(err) => {
-                 config.new_build_error(
-diff --git a/crates/store/src/backend/http/lookup.rs b/crates/store/src/backend/http/lookup.rs
-index dbaa932ed..ff7cca2d8 100644
---- a/crates/store/src/backend/http/lookup.rs
-+++ b/crates/store/src/backend/http/lookup.rs
-@@ -91,7 +91,7 @@ impl HttpStore {
-     async fn try_refresh(&self) -> trc::Result<AHashMap<String, Value<'static>>> {
-         let time = Instant::now();
-         let agent = BROWSER_USER_AGENTS.choose(&mut rand::rng()).unwrap();
--        let response = reqwest::Client::builder()
-+        let response = utils::reqwest_client_builder()
-             .timeout(self.config.timeout)
-             .user_agent(*agent)
-             .build()
-diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml
-index 61d1cd69a..22d8e6599 100644
---- a/crates/trc/Cargo.toml
-+++ b/crates/trc/Cargo.toml
-@@ -11,7 +11,7 @@ mail-parser = { version = "0.11", features = ["full_encoding"] }
- base64 = "0.22.1"
- serde = "1.0"
- serde_json = "1.0.120"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
-+reqwest = { version = "0.12", default-features = false, features = ["http2"]}
- rtrb = "0.3.1"
- parking_lot = "0.12.3"
- tokio = { version = "1.45", features = ["net", "macros"] }
-diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
-index 4766c55b5..f45780d24 100644
---- a/crates/utils/Cargo.toml
-+++ b/crates/utils/Cargo.toml
-@@ -9,6 +9,7 @@ trc = { path = "../trc" }
- rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
- rustls-pemfile = "2.0"
- rustls-pki-types = { version = "1" }
-+rustls-platform-verifier = { version = "0.5.3", optional = true }
- tokio = { version = "1.45", features = ["net", "macros", "signal"] }
- tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
- serde = { version = "1.0", features = ["derive"]}
-@@ -45,6 +46,7 @@ privdrop = "0.5.3"
- 
- [features]
- test_mode = []
-+tls-native-roots = ["dep:rustls-platform-verifier", "reqwest/rustls-tls-native-roots"]
- 
- [dev-dependencies]
- tokio = { version = "1.45", features = ["full"] }
-diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
-index 2b73df148..2efcc96d7 100644
---- a/crates/utils/src/lib.rs
-+++ b/crates/utils/src/lib.rs
-@@ -21,14 +21,14 @@ use compact_str::ToCompactString;
- use futures::StreamExt;
- use reqwest::Response;
- use rustls::{
--    ClientConfig, RootCertStore, SignatureScheme,
-+    ClientConfig, SignatureScheme,
-     client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
- };
--use rustls_pki_types::TrustAnchor;
- 
- pub use downcast_rs;
- pub use erased_serde;
- 
-+pub static USER_AGENT: &str = "Stalwart/1.0.0";
- pub const BLOB_HASH_LEN: usize = 32;
- 
- #[derive(
-@@ -294,20 +294,28 @@ pub async fn wait_for_shutdown() {
- }
- 
- pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig {
-+    #[cfg(feature = "tls-native-roots")]
-+    use rustls_platform_verifier::BuilderVerifierExt;
-+
-     let config = ClientConfig::builder();
- 
-     if !allow_invalid_certs {
--        let mut root_cert_store = RootCertStore::empty();
--
--        root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| TrustAnchor {
--            subject: ta.subject.clone(),
--            subject_public_key_info: ta.subject_public_key_info.clone(),
--            name_constraints: ta.name_constraints.clone(),
--        }));
--
--        config
--            .with_root_certificates(root_cert_store)
--            .with_no_client_auth()
-+        #[cfg(feature = "tls-native-roots")]
-+        let config = config.with_platform_verifier();
-+
-+        #[cfg(not(feature = "tls-native-roots"))]
-+        let config = config.with_root_certificates(
-+            webpki_roots::TLS_SERVER_ROOTS
-+                .iter()
-+                .map(|ta| rustls_pki_types::TrustAnchor {
-+                    subject: ta.subject.clone(),
-+                    subject_public_key_info: ta.subject_public_key_info.clone(),
-+                    name_constraints: ta.name_constraints.clone(),
-+                })
-+                .collect::<rustls::RootCertStore>(),
-+        );
-+
-+        config.with_no_client_auth()
-     } else {
-         config
-             .dangerous()
-@@ -316,6 +324,18 @@ pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig {
-     }
- }
- 
-+pub fn reqwest_client_builder() -> reqwest::ClientBuilder {
-+    let builder = reqwest::Client::builder();
-+
-+    #[cfg(feature = "tls-native-roots")]
-+    let builder = builder.tls_built_in_native_certs(true);
-+
-+    #[cfg(not(feature = "tls-native-roots"))]
-+    let builder = builder.tls_built_in_webpki_certs(true);
-+
-+    builder.user_agent(USER_AGENT)
-+}
-+
- #[derive(Debug)]
- struct DummyVerifier;
- 
-diff --git a/crates/utils/src/suffixlist.rs b/crates/utils/src/suffixlist.rs
-index e0921abe3..b53126ae9 100644
---- a/crates/utils/src/suffixlist.rs
-+++ b/crates/utils/src/suffixlist.rs
-@@ -111,7 +111,12 @@ impl PublicSuffix {
- 
-         for (idx, value) in values.into_iter().enumerate() {
-             let bytes = if value.starts_with("https://") || value.starts_with("http://") {
--                let result = match reqwest::get(&value).await {
-+                let result = match crate::reqwest_client_builder()
-+                    .build()
-+                    .unwrap_or_default()
-+                    .get(&value)
-+                    .await
-+                {
-                     Ok(r) => {
-                         if r.status().is_success() {
-                             r.bytes().await
-diff --git a/tests/Cargo.toml b/tests/Cargo.toml
-index 386b8255a..68692e939 100644
---- a/tests/Cargo.toml
-+++ b/tests/Cargo.toml
-@@ -59,7 +59,7 @@ rayon = { version = "1.5.1" }
- flate2 = { version = "1.0.17", features = ["zlib"], default-features = false }
- serde = { version = "1.0", features = ["derive"]}
- serde_json = "1.0"
--reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "multipart", "http2"]}
-+reqwest = { version = "0.12", default-features = false, features = ["multipart", "http2"]}
- bytes = "1.4.0"
- futures = "0.3"
- ece = "2.2"
-diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs
-index 0d05d3a77..fff811fcf 100644
---- a/tests/src/jmap/auth_oauth.rs
-+++ b/tests/src/jmap/auth_oauth.rs
-@@ -411,7 +411,7 @@ async fn post_bytes(
-     auth_token: Option<&str>,
-     params: &AHashMap<String, String>,
- ) -> Bytes {
--    let mut client = reqwest::Client::builder()
-+    let mut client = utils::reqwest_client_builder()
-         .timeout(Duration::from_millis(500))
-         .danger_accept_invalid_certs(true)
-         .build()
-@@ -437,7 +437,7 @@ async fn post_json<D: DeserializeOwned>(
-     auth_token: Option<&str>,
-     body: &impl Serialize,
- ) -> D {
--    let mut client = reqwest::Client::builder()
-+    let mut client = utils::reqwest_client_builder()
-         .timeout(Duration::from_millis(500))
-         .danger_accept_invalid_certs(true)
-         .build()
-@@ -473,7 +473,7 @@ async fn post_with_auth<T: DeserializeOwned>(
- }
- 
- async fn get_bytes(url: &str) -> Bytes {
--    reqwest::Client::builder()
-+    utils::reqwest_client_builder()
-         .timeout(Duration::from_millis(500))
-         .danger_accept_invalid_certs(true)
-         .build()
-diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs
-index 554026a00..ffd8d292a 100644
---- a/tests/src/jmap/mod.rs
-+++ b/tests/src/jmap/mod.rs
-@@ -419,7 +419,7 @@ pub async fn jmap_raw_request(body: impl AsRef<str>, username: &str, secret: &st
-       }"#;
- 
-     String::from_utf8(
--        reqwest::Client::builder()
-+        utils::reqwest_client_builder()
-             .danger_accept_invalid_certs(true)
-             .timeout(Duration::from_millis(1000))
-             .default_headers(headers)
-@@ -620,7 +620,7 @@ impl ManagementApi {
-         query: &str,
-         body: Option<String>,
-     ) -> Result<String, String> {
--        let mut request = reqwest::Client::builder()
-+        let mut request = utils::reqwest_client_builder()
-             .timeout(Duration::from_millis(500))
-             .danger_accept_invalid_certs(true)
-             .build()
-diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs
-index e86f55169..a0d144127 100644
---- a/tests/src/smtp/management/queue.rs
-+++ b/tests/src/smtp/management/queue.rs
-@@ -493,7 +493,7 @@ async fn manage_queue() {
- 
-     // Test authentication error
-     assert_eq!(
--        reqwest::Client::builder()
-+        utils::reqwest_client_builder()
-             .timeout(Duration::from_millis(500))
-             .danger_accept_invalid_certs(true)
-             .build()
-diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs
-index 0ed8b1888..25aab71a4 100644
---- a/tests/src/webdav/mod.rs
-+++ b/tests/src/webdav/mod.rs
-@@ -302,7 +302,7 @@ impl DummyWebDavClient {
-         headers: impl IntoIterator<Item = (&'static str, &str)>,
-         body: impl Into<String>,
-     ) -> DavResponse {
--        let mut request = reqwest::Client::builder()
-+        let mut request = utils::reqwest_client_builder()
-             .timeout(Duration::from_millis(500))
-             .danger_accept_invalid_certs(true)
-             .build()