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, 747 insertions, 0 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
new file mode 100644
index 0000000..392fbde
--- /dev/null
+++ b/pkgs/by-name/st/stalwart-mail-patched/patches/use-platform-ca-roots.patch
@@ -0,0 +1,747 @@
+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()