Scope --allow-invalid-certs to the server the user named
The flag switched certificate checks off for every connection in the run. That included the Microsoft sign-in endpoints, so a user passing it for a self-signed source also sent refresh tokens, device codes and EWS client secrets over unverified TLS. It also covered the export target, and any host a server redirected to or named for its API, uploads or downloads. It now applies only where the user pointed it: the host of --url, for the source of an import or the target of an export. For an Exchange import with no --url, it covers the mailbox's own domain, where on-premises Autodiscover looks, and then only the EWS endpoint Autodiscover finds. The Microsoft and Google sign-in and cloud hosts are always verified, with or without the flag. Each HTTP client keeps a verifying agent and, only when the flag applies, a second one that accepts invalid certificates, and picks per request by host. The sign-in modules no longer take the flag at all. Autodiscover v2, which is Microsoft's own service, is always verified.
This commit is contained in:
+20
-1
@@ -26,7 +26,26 @@ policy, TLS handling -- besides its own. The ones that matter most:
|
|||||||
| `-v`, `-vv`, `-vvv` | Increase log verbosity. |
|
| `-v`, `-vv`, `-vvv` | Increase log verbosity. |
|
||||||
| `-q, --quiet` | Warnings and errors only. |
|
| `-q, --quiet` | Warnings and errors only. |
|
||||||
| `--max-retries <N>` | Max retries per request on transient failures (default 5). |
|
| `--max-retries <N>` | Max retries per request on transient failures (default 5). |
|
||||||
| `--allow-invalid-certs` | Accept self-signed / invalid TLS certs. |
|
| `--allow-invalid-certs` | Accept a self-signed or otherwise invalid certificate from the server named by `--url` (see below). |
|
||||||
|
|
||||||
|
### Invalid certificates
|
||||||
|
|
||||||
|
`--allow-invalid-certs` is for a server with a self-signed certificate, and
|
||||||
|
it applies to that server only: the host in `--url`, whether that is the
|
||||||
|
source of an import or the target of an export. For an Exchange import with
|
||||||
|
no `--url`, it covers the mailbox's own domain and the hosts under it, which
|
||||||
|
is where on-premises Autodiscover looks, and then only the EWS endpoint
|
||||||
|
Autodiscover finds.
|
||||||
|
|
||||||
|
Every other host is verified as usual, including a host the server
|
||||||
|
redirects to or names for its API, uploads or downloads. The Microsoft and
|
||||||
|
Google sign-in and cloud endpoints are always verified, with or without the
|
||||||
|
flag: a certificate that fails there is an attack or a broken network, never
|
||||||
|
a server to trust.
|
||||||
|
|
||||||
|
Connections time out rather than wait forever: 30 seconds to connect, 5
|
||||||
|
minutes for the server's first byte, and 30 minutes to read a whole
|
||||||
|
response. A timed-out request is retried like any other transient failure.
|
||||||
|
|
||||||
Secrets come from the `INBUXA_MIGRATE_*` environment variables or a prompt;
|
Secrets come from the `INBUXA_MIGRATE_*` environment variables or a prompt;
|
||||||
see [Credentials](../README.md#credentials). The command line takes them too,
|
see [Credentials](../README.md#credentials). The command line takes them too,
|
||||||
|
|||||||
+4
-1
@@ -122,7 +122,10 @@ struct GlobalArgs {
|
|||||||
)]
|
)]
|
||||||
max_retries: u32,
|
max_retries: u32,
|
||||||
|
|
||||||
#[arg(long, help = "Accept self-signed / invalid TLS certificates")]
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Accept an invalid TLS certificate from the --url host only; sign-in endpoints are always verified"
|
||||||
|
)]
|
||||||
allow_invalid_certs: bool,
|
allow_invalid_certs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-15
@@ -21,7 +21,7 @@ use crate::jmap::error::JmapError;
|
|||||||
use crate::jmap::http::{Auth, RetryPolicy, retry_after_header};
|
use crate::jmap::http::{Auth, RetryPolicy, retry_after_header};
|
||||||
use crate::jmap::retry::{self, RateLimitState};
|
use crate::jmap::retry::{self, RateLimitState};
|
||||||
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
||||||
use crate::net::{tls, with_timeouts};
|
use crate::net::{CertOverride, tls, with_timeouts};
|
||||||
|
|
||||||
const MAX_BODY: u64 = 512 * 1024 * 1024;
|
const MAX_BODY: u64 = 512 * 1024 * 1024;
|
||||||
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
||||||
@@ -48,6 +48,8 @@ pub struct MultiStatus {
|
|||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
lax_agent: Option<Agent>,
|
||||||
|
certs: CertOverride,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
retry: RetryPolicy,
|
retry: RetryPolicy,
|
||||||
rate_limit: RateLimitState,
|
rate_limit: RateLimitState,
|
||||||
@@ -57,25 +59,42 @@ struct Inner {
|
|||||||
user_agent: String,
|
user_agent: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
/// The agent for `url`: the one that accepts invalid certificates only for
|
||||||
|
/// a host `--allow-invalid-certs` covers, and the verifying one otherwise.
|
||||||
|
fn agent_for(&self, url: &str) -> &Agent {
|
||||||
|
match &self.lax_agent {
|
||||||
|
Some(lax) if self.certs.allows(url) => lax,
|
||||||
|
_ => &self.agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DavClient {
|
pub struct DavClient {
|
||||||
inner: Arc<Inner>,
|
inner: Arc<Inner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DavClient {
|
impl DavClient {
|
||||||
pub fn new(auth: Auth, retry: RetryPolicy, allow_invalid_certs: bool) -> Self {
|
pub fn new(auth: Auth, retry: RetryPolicy, certs: CertOverride) -> Self {
|
||||||
let config: Config = with_timeouts!(
|
let build = |accept_invalid: bool| -> Agent {
|
||||||
Config::builder()
|
let config: Config = with_timeouts!(
|
||||||
.http_status_as_error(false)
|
Config::builder()
|
||||||
.allow_non_standard_methods(true)
|
.http_status_as_error(false)
|
||||||
.max_redirects(0)
|
.allow_non_standard_methods(true)
|
||||||
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
.max_redirects(0)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
||||||
)
|
.tls_config(tls(accept_invalid))
|
||||||
.build();
|
)
|
||||||
|
.build();
|
||||||
|
config.new_agent()
|
||||||
|
};
|
||||||
|
let lax_agent = certs.is_active().then(|| build(true));
|
||||||
DavClient {
|
DavClient {
|
||||||
inner: Arc::new(Inner {
|
inner: Arc::new(Inner {
|
||||||
agent: config.new_agent(),
|
agent: build(false),
|
||||||
|
lax_agent,
|
||||||
|
certs,
|
||||||
auth,
|
auth,
|
||||||
retry,
|
retry,
|
||||||
rate_limit: RateLimitState::new(),
|
rate_limit: RateLimitState::new(),
|
||||||
@@ -816,7 +835,7 @@ impl DavClient {
|
|||||||
let request = builder
|
let request = builder
|
||||||
.body(payload)
|
.body(payload)
|
||||||
.map_err(|e| ureq::Error::Other(Box::new(std::io::Error::other(e))))?;
|
.map_err(|e| ureq::Error::Other(Box::new(std::io::Error::other(e))))?;
|
||||||
self.inner.agent.run(request)
|
self.inner.agent_for(req.url).run(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +954,7 @@ fn truncate(body: &[u8]) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_timeout_is_a_retryable_transport_error() {
|
fn every_timeout_is_a_retryable_transport_error() {
|
||||||
@@ -962,7 +982,7 @@ mod tests {
|
|||||||
password: "p".into(),
|
password: "p".into(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(3),
|
RetryPolicy::new(3),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
assert_eq!(c.retries_observed(), 0);
|
assert_eq!(c.retries_observed(), 0);
|
||||||
assert_eq!(c.retry_after_sleeps(), 0);
|
assert_eq!(c.retry_after_sleeps(), 0);
|
||||||
@@ -975,7 +995,7 @@ mod tests {
|
|||||||
token: "abc".into(),
|
token: "abc".into(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
let logger = c.logger();
|
let logger = c.logger();
|
||||||
assert_eq!(logger.level(), LEVEL_DEFAULT);
|
assert_eq!(logger.level(), LEVEL_DEFAULT);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use ureq::config::Config;
|
|||||||
|
|
||||||
use crate::exchange_ews::error::EwsError;
|
use crate::exchange_ews::error::EwsError;
|
||||||
use crate::exchange_ews::parse::entity_to_char;
|
use crate::exchange_ews::parse::entity_to_char;
|
||||||
use crate::net::{tls, with_timeouts};
|
use crate::net::{CertOverride, tls, with_timeouts};
|
||||||
|
|
||||||
const V2_HOST: &str = "https://outlook.office365.com";
|
const V2_HOST: &str = "https://outlook.office365.com";
|
||||||
const POX_REQ_NS: &str =
|
const POX_REQ_NS: &str =
|
||||||
@@ -49,7 +49,7 @@ pub fn discover(
|
|||||||
supplied_url: Option<&str>,
|
supplied_url: Option<&str>,
|
||||||
email: Option<&str>,
|
email: Option<&str>,
|
||||||
auth_header: Option<&str>,
|
auth_header: Option<&str>,
|
||||||
allow_invalid_certs: bool,
|
certs: &CertOverride,
|
||||||
) -> Result<DiscoveryResult, EwsError> {
|
) -> Result<DiscoveryResult, EwsError> {
|
||||||
if let Some(url) = supplied_url
|
if let Some(url) = supplied_url
|
||||||
&& is_fully_qualified_ews_url(url)
|
&& is_fully_qualified_ews_url(url)
|
||||||
@@ -64,8 +64,17 @@ pub fn discover(
|
|||||||
"either a fully-qualified --url or --mailbox is required".to_owned(),
|
"either a fully-qualified --url or --mailbox is required".to_owned(),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let agent = build_agent(allow_invalid_certs);
|
// Autodiscover v2 is Microsoft's own service and is always verified; a v1
|
||||||
if let Ok(url) = autodiscover_v2(&agent, email) {
|
// candidate gets the relaxed agent only if `--allow-invalid-certs` covers it.
|
||||||
|
let strict = build_agent(false);
|
||||||
|
let lax = certs.is_active().then(|| build_agent(true));
|
||||||
|
let pick = |url: &str| -> &Agent {
|
||||||
|
match &lax {
|
||||||
|
Some(agent) if certs.allows(url) => agent,
|
||||||
|
_ => &strict,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Ok(url) = autodiscover_v2(&strict, email) {
|
||||||
return Ok(DiscoveryResult {
|
return Ok(DiscoveryResult {
|
||||||
ews_url: url,
|
ews_url: url,
|
||||||
source: DiscoverySource::V2,
|
source: DiscoverySource::V2,
|
||||||
@@ -83,7 +92,7 @@ pub fn discover(
|
|||||||
let candidates = pox_candidates(domain);
|
let candidates = pox_candidates(domain);
|
||||||
for candidate in &candidates {
|
for candidate in &candidates {
|
||||||
tried.push(candidate.clone());
|
tried.push(candidate.clone());
|
||||||
match autodiscover_v1(&agent, candidate, ¤t_email, auth_header) {
|
match autodiscover_v1(pick(candidate), candidate, ¤t_email, auth_header) {
|
||||||
Ok(PoxOutcome::EwsUrl(url)) => {
|
Ok(PoxOutcome::EwsUrl(url)) => {
|
||||||
return Ok(DiscoveryResult {
|
return Ok(DiscoveryResult {
|
||||||
ews_url: url,
|
ews_url: url,
|
||||||
@@ -105,7 +114,7 @@ pub fn discover(
|
|||||||
url_redirects += 1;
|
url_redirects += 1;
|
||||||
tried.push(url.clone());
|
tried.push(url.clone());
|
||||||
if let Ok(PoxOutcome::EwsUrl(u)) =
|
if let Ok(PoxOutcome::EwsUrl(u)) =
|
||||||
autodiscover_v1(&agent, &url, ¤t_email, auth_header)
|
autodiscover_v1(pick(&url), &url, ¤t_email, auth_header)
|
||||||
{
|
{
|
||||||
return Ok(DiscoveryResult {
|
return Ok(DiscoveryResult {
|
||||||
ews_url: u,
|
ews_url: u,
|
||||||
@@ -131,11 +140,11 @@ pub fn discover(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_agent(allow_invalid_certs: bool) -> Agent {
|
fn build_agent(accept_invalid: bool) -> Agent {
|
||||||
let config: Config = with_timeouts!(
|
let config: Config = with_timeouts!(
|
||||||
Config::builder()
|
Config::builder()
|
||||||
.http_status_as_error(false)
|
.http_status_as_error(false)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.tls_config(tls(accept_invalid))
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
config.new_agent()
|
config.new_agent()
|
||||||
|
|||||||
+34
-14
@@ -21,13 +21,15 @@ use crate::exchange_ews::types::ServerVersion;
|
|||||||
use crate::jmap::http::{Auth, RetryPolicy, retry_after_header};
|
use crate::jmap::http::{Auth, RetryPolicy, retry_after_header};
|
||||||
use crate::jmap::retry::{self, Disposition, RateLimitState};
|
use crate::jmap::retry::{self, Disposition, RateLimitState};
|
||||||
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
||||||
use crate::net::{tls, with_timeouts};
|
use crate::net::{CertOverride, tls, with_timeouts};
|
||||||
|
|
||||||
const MAX_BODY: u64 = 2 * 1024 * 1024 * 1024;
|
const MAX_BODY: u64 = 2 * 1024 * 1024 * 1024;
|
||||||
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
lax_agent: Option<Agent>,
|
||||||
|
certs: CertOverride,
|
||||||
auth: Mutex<Auth>,
|
auth: Mutex<Auth>,
|
||||||
impersonated_smtp: Mutex<Option<String>>,
|
impersonated_smtp: Mutex<Option<String>>,
|
||||||
anchor_mailbox: Mutex<Option<String>>,
|
anchor_mailbox: Mutex<Option<String>>,
|
||||||
@@ -42,6 +44,17 @@ struct Inner {
|
|||||||
user_agent: String,
|
user_agent: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
/// The agent for `url`: the one that accepts invalid certificates only for
|
||||||
|
/// a host `--allow-invalid-certs` covers, and the verifying one otherwise.
|
||||||
|
fn agent_for(&self, url: &str) -> &Agent {
|
||||||
|
match &self.lax_agent {
|
||||||
|
Some(lax) if self.certs.allows(url) => lax,
|
||||||
|
_ => &self.agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EwsClient {
|
pub struct EwsClient {
|
||||||
inner: Arc<Inner>,
|
inner: Arc<Inner>,
|
||||||
@@ -54,17 +67,23 @@ pub struct SoapResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EwsClient {
|
impl EwsClient {
|
||||||
pub fn new(auth: Auth, retry: RetryPolicy, allow_invalid_certs: bool) -> EwsClient {
|
pub fn new(auth: Auth, retry: RetryPolicy, certs: CertOverride) -> EwsClient {
|
||||||
let config: Config = with_timeouts!(
|
let build = |accept_invalid: bool| -> Agent {
|
||||||
Config::builder()
|
let config: Config = with_timeouts!(
|
||||||
.http_status_as_error(false)
|
Config::builder()
|
||||||
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
.http_status_as_error(false)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
||||||
)
|
.tls_config(tls(accept_invalid))
|
||||||
.build();
|
)
|
||||||
|
.build();
|
||||||
|
config.new_agent()
|
||||||
|
};
|
||||||
|
let lax_agent = certs.is_active().then(|| build(true));
|
||||||
EwsClient {
|
EwsClient {
|
||||||
inner: Arc::new(Inner {
|
inner: Arc::new(Inner {
|
||||||
agent: config.new_agent(),
|
agent: build(false),
|
||||||
|
lax_agent,
|
||||||
|
certs,
|
||||||
auth: Mutex::new(auth),
|
auth: Mutex::new(auth),
|
||||||
impersonated_smtp: Mutex::new(None),
|
impersonated_smtp: Mutex::new(None),
|
||||||
anchor_mailbox: Mutex::new(None),
|
anchor_mailbox: Mutex::new(None),
|
||||||
@@ -402,7 +421,7 @@ impl EwsClient {
|
|||||||
fn one_attempt(&self, url: &str, body: &str, action: &str) -> AttemptOutcome {
|
fn one_attempt(&self, url: &str, body: &str, action: &str) -> AttemptOutcome {
|
||||||
let mut req = self
|
let mut req = self
|
||||||
.inner
|
.inner
|
||||||
.agent
|
.agent_for(url)
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("Authorization", self.auth_header())
|
.header("Authorization", self.auth_header())
|
||||||
.header("Content-Type", "text/xml; charset=utf-8")
|
.header("Content-Type", "text/xml; charset=utf-8")
|
||||||
@@ -529,6 +548,7 @@ fn truncate(body: &[u8]) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_timeout_is_a_transport_error_and_so_retried() {
|
fn every_timeout_is_a_transport_error_and_so_retried() {
|
||||||
@@ -550,7 +570,7 @@ mod tests {
|
|||||||
let c = EwsClient::new(
|
let c = EwsClient::new(
|
||||||
Auth::Bearer { token: "t".into() },
|
Auth::Bearer { token: "t".into() },
|
||||||
RetryPolicy::new(3),
|
RetryPolicy::new(3),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
assert_eq!(c.server_version(), ServerVersion::Exchange2013Sp1);
|
assert_eq!(c.server_version(), ServerVersion::Exchange2013Sp1);
|
||||||
assert_eq!(c.retries_observed(), 0);
|
assert_eq!(c.retries_observed(), 0);
|
||||||
@@ -562,7 +582,7 @@ mod tests {
|
|||||||
let c = EwsClient::new(
|
let c = EwsClient::new(
|
||||||
Auth::Bearer { token: "t".into() },
|
Auth::Bearer { token: "t".into() },
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
c.set_server_version(ServerVersion::Exchange2019);
|
c.set_server_version(ServerVersion::Exchange2019);
|
||||||
assert_eq!(c.server_version(), ServerVersion::Exchange2019);
|
assert_eq!(c.server_version(), ServerVersion::Exchange2019);
|
||||||
@@ -573,7 +593,7 @@ mod tests {
|
|||||||
let c = EwsClient::new(
|
let c = EwsClient::new(
|
||||||
Auth::Bearer { token: "t".into() },
|
Auth::Bearer { token: "t".into() },
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
c.set_anchor_mailbox(Some("alice@x".to_owned()));
|
c.set_anchor_mailbox(Some("alice@x".to_owned()));
|
||||||
assert_eq!(c.anchor_header().as_deref(), Some("alice@x"));
|
assert_eq!(c.anchor_header().as_deref(), Some("alice@x"));
|
||||||
|
|||||||
+12
-23
@@ -47,7 +47,7 @@ pub enum OAuthFlow {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn acquire(flow: &OAuthFlow, allow_invalid_certs: bool) -> Result<AcquiredToken, EwsError> {
|
pub fn acquire(flow: &OAuthFlow) -> Result<AcquiredToken, EwsError> {
|
||||||
match flow {
|
match flow {
|
||||||
OAuthFlow::PreAcquired { token } => {
|
OAuthFlow::PreAcquired { token } => {
|
||||||
let claims = decode_jwt_claims(token).unwrap_or_default();
|
let claims = decode_jwt_claims(token).unwrap_or_default();
|
||||||
@@ -64,10 +64,8 @@ pub fn acquire(flow: &OAuthFlow, allow_invalid_certs: bool) -> Result<AcquiredTo
|
|||||||
tenant,
|
tenant,
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
} => client_credentials(tenant, client_id, client_secret, allow_invalid_certs),
|
} => client_credentials(tenant, client_id, client_secret),
|
||||||
OAuthFlow::DeviceCode { tenant, client_id } => {
|
OAuthFlow::DeviceCode { tenant, client_id } => device_code_flow(tenant, client_id),
|
||||||
device_code_flow(tenant, client_id, allow_invalid_certs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +103,11 @@ fn device_code_endpoint(tenant: &str) -> String {
|
|||||||
format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode")
|
format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_agent(allow_invalid_certs: bool) -> ureq::Agent {
|
fn build_agent() -> ureq::Agent {
|
||||||
let config: Config = with_timeouts!(
|
let config: Config = with_timeouts!(
|
||||||
Config::builder()
|
Config::builder()
|
||||||
.http_status_as_error(false)
|
.http_status_as_error(false)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.tls_config(tls(false))
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
config.new_agent()
|
config.new_agent()
|
||||||
@@ -119,9 +117,8 @@ fn client_credentials(
|
|||||||
tenant: &str,
|
tenant: &str,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
client_secret: &str,
|
client_secret: &str,
|
||||||
allow_invalid_certs: bool,
|
|
||||||
) -> Result<AcquiredToken, EwsError> {
|
) -> Result<AcquiredToken, EwsError> {
|
||||||
let agent = build_agent(allow_invalid_certs);
|
let agent = build_agent();
|
||||||
let body = form_encode(&[
|
let body = form_encode(&[
|
||||||
("client_id", client_id),
|
("client_id", client_id),
|
||||||
("client_secret", client_secret),
|
("client_secret", client_secret),
|
||||||
@@ -137,12 +134,8 @@ fn client_credentials(
|
|||||||
parse_token_response(resp)
|
parse_token_response(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn device_code_flow(
|
fn device_code_flow(tenant: &str, client_id: &str) -> Result<AcquiredToken, EwsError> {
|
||||||
tenant: &str,
|
let agent = build_agent();
|
||||||
client_id: &str,
|
|
||||||
allow_invalid_certs: bool,
|
|
||||||
) -> Result<AcquiredToken, EwsError> {
|
|
||||||
let agent = build_agent(allow_invalid_certs);
|
|
||||||
let body = form_encode(&[("client_id", client_id), ("scope", SCOPE_DELEGATED)]);
|
let body = form_encode(&[("client_id", client_id), ("scope", SCOPE_DELEGATED)]);
|
||||||
let endpoint = device_code_endpoint(tenant);
|
let endpoint = device_code_endpoint(tenant);
|
||||||
let mut resp = agent
|
let mut resp = agent
|
||||||
@@ -274,9 +267,8 @@ pub fn refresh_with_token(
|
|||||||
tenant: &str,
|
tenant: &str,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
refresh_token: &str,
|
refresh_token: &str,
|
||||||
allow_invalid_certs: bool,
|
|
||||||
) -> Result<AcquiredToken, EwsError> {
|
) -> Result<AcquiredToken, EwsError> {
|
||||||
let agent = build_agent(allow_invalid_certs);
|
let agent = build_agent();
|
||||||
let body = form_encode(&[
|
let body = form_encode(&[
|
||||||
("client_id", client_id),
|
("client_id", client_id),
|
||||||
("grant_type", "refresh_token"),
|
("grant_type", "refresh_token"),
|
||||||
@@ -361,12 +353,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pre_acquired_flow_decodes_claims() {
|
fn pre_acquired_flow_decodes_claims() {
|
||||||
let token = make_jwt("t-2", "bob@x", 9999999999);
|
let token = make_jwt("t-2", "bob@x", 9999999999);
|
||||||
let acq = acquire(
|
let acq = acquire(&OAuthFlow::PreAcquired {
|
||||||
&OAuthFlow::PreAcquired {
|
token: token.clone(),
|
||||||
token: token.clone(),
|
})
|
||||||
},
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(acq.access_token, token);
|
assert_eq!(acq.access_token, token);
|
||||||
assert_eq!(acq.tenant_id.as_deref(), Some("t-2"));
|
assert_eq!(acq.tenant_id.as_deref(), Some("t-2"));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::exchange_graph::retry::{HttpClass, classify_http_status, is_throttled
|
|||||||
use crate::jmap::http::{RetryPolicy, cross_host, retry_after_header};
|
use crate::jmap::http::{RetryPolicy, cross_host, retry_after_header};
|
||||||
use crate::jmap::retry::{self, RateLimitState};
|
use crate::jmap::retry::{self, RateLimitState};
|
||||||
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
||||||
use crate::net::{tls, with_timeouts};
|
use crate::net::{CertOverride, tls, with_timeouts};
|
||||||
|
|
||||||
const MAX_BODY: u64 = 256 * 1024 * 1024;
|
const MAX_BODY: u64 = 256 * 1024 * 1024;
|
||||||
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
||||||
@@ -63,6 +63,8 @@ impl GraphResponse {
|
|||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
lax_agent: Option<Agent>,
|
||||||
|
certs: CertOverride,
|
||||||
bearer: Mutex<String>,
|
bearer: Mutex<String>,
|
||||||
retry: RetryPolicy,
|
retry: RetryPolicy,
|
||||||
rate_limit: RateLimitState,
|
rate_limit: RateLimitState,
|
||||||
@@ -73,6 +75,17 @@ struct Inner {
|
|||||||
user_agent: String,
|
user_agent: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
/// The agent for `url`: the one that accepts invalid certificates only for
|
||||||
|
/// a host `--allow-invalid-certs` covers, and the verifying one otherwise.
|
||||||
|
fn agent_for(&self, url: &str) -> &Agent {
|
||||||
|
match &self.lax_agent {
|
||||||
|
Some(lax) if self.certs.allows(url) => lax,
|
||||||
|
_ => &self.agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GraphClient {
|
pub struct GraphClient {
|
||||||
inner: Arc<Inner>,
|
inner: Arc<Inner>,
|
||||||
@@ -89,17 +102,23 @@ enum Attempt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GraphClient {
|
impl GraphClient {
|
||||||
pub fn new(bearer: String, retry: RetryPolicy, allow_invalid_certs: bool) -> GraphClient {
|
pub fn new(bearer: String, retry: RetryPolicy, certs: CertOverride) -> GraphClient {
|
||||||
let config: Config = with_timeouts!(
|
let build = |accept_invalid: bool| -> Agent {
|
||||||
Config::builder()
|
let config: Config = with_timeouts!(
|
||||||
.http_status_as_error(false)
|
Config::builder()
|
||||||
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
.http_status_as_error(false)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
||||||
)
|
.tls_config(tls(accept_invalid))
|
||||||
.build();
|
)
|
||||||
|
.build();
|
||||||
|
config.new_agent()
|
||||||
|
};
|
||||||
|
let lax_agent = certs.is_active().then(|| build(true));
|
||||||
GraphClient {
|
GraphClient {
|
||||||
inner: Arc::new(Inner {
|
inner: Arc::new(Inner {
|
||||||
agent: config.new_agent(),
|
agent: build(false),
|
||||||
|
lax_agent,
|
||||||
|
certs,
|
||||||
bearer: Mutex::new(bearer),
|
bearer: Mutex::new(bearer),
|
||||||
retry,
|
retry,
|
||||||
rate_limit: RateLimitState::new(),
|
rate_limit: RateLimitState::new(),
|
||||||
@@ -304,7 +323,7 @@ impl GraphClient {
|
|||||||
extra_prefer: &[&str],
|
extra_prefer: &[&str],
|
||||||
) -> Attempt {
|
) -> Attempt {
|
||||||
let mut req = match method {
|
let mut req = match method {
|
||||||
"GET" => self.inner.agent.get(url),
|
"GET" => self.inner.agent_for(url).get(url),
|
||||||
other => {
|
other => {
|
||||||
return Attempt::Transport(GraphError::Connect(format!(
|
return Attempt::Transport(GraphError::Connect(format!(
|
||||||
"unsupported method {other} (graph importer is read-only)"
|
"unsupported method {other} (graph importer is read-only)"
|
||||||
@@ -468,6 +487,7 @@ fn format_retry_wait(d: Duration) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_timeout_is_a_transport_error_and_so_retried() {
|
fn every_timeout_is_a_transport_error_and_so_retried() {
|
||||||
@@ -486,7 +506,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn defaults_construct() {
|
fn defaults_construct() {
|
||||||
let c = GraphClient::new("token".to_owned(), RetryPolicy::new(3), false);
|
let c = GraphClient::new(
|
||||||
|
"token".to_owned(),
|
||||||
|
RetryPolicy::new(3),
|
||||||
|
CertOverride::none(),
|
||||||
|
);
|
||||||
assert_eq!(c.retries_observed(), 0);
|
assert_eq!(c.retries_observed(), 0);
|
||||||
assert_eq!(c.retry_after_sleeps(), 0);
|
assert_eq!(c.retry_after_sleeps(), 0);
|
||||||
assert_eq!(c.requests_observed(), 0);
|
assert_eq!(c.requests_observed(), 0);
|
||||||
@@ -495,7 +519,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bearer_can_be_swapped_at_runtime() {
|
fn bearer_can_be_swapped_at_runtime() {
|
||||||
let c = GraphClient::new("old".to_owned(), RetryPolicy::new(0), false);
|
let c = GraphClient::new("old".to_owned(), RetryPolicy::new(0), CertOverride::none());
|
||||||
c.set_bearer("new".to_owned());
|
c.set_bearer("new".to_owned());
|
||||||
assert_eq!(c.auth_header(), "Bearer new");
|
assert_eq!(c.auth_header(), "Bearer new");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,23 +79,23 @@ pub struct AcquiredToken {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_agent(allow_invalid_certs: bool) -> ureq::Agent {
|
fn build_agent() -> ureq::Agent {
|
||||||
let config: Config = with_timeouts!(
|
let config: Config = with_timeouts!(
|
||||||
Config::builder()
|
Config::builder()
|
||||||
.http_status_as_error(false)
|
.http_status_as_error(false)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.tls_config(tls(false))
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
config.new_agent()
|
config.new_agent()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn acquire(flow: &OAuthFlow, allow_invalid_certs: bool) -> Result<AcquiredToken, GraphError> {
|
pub fn acquire(flow: &OAuthFlow) -> Result<AcquiredToken, GraphError> {
|
||||||
match flow {
|
match flow {
|
||||||
OAuthFlow::PreAcquired { token } => Ok(token_from_string(token.clone())),
|
OAuthFlow::PreAcquired { token } => Ok(token_from_string(token.clone())),
|
||||||
OAuthFlow::DeviceCode {
|
OAuthFlow::DeviceCode {
|
||||||
authority,
|
authority,
|
||||||
client_id,
|
client_id,
|
||||||
} => device_code_flow(authority, client_id, allow_invalid_certs),
|
} => device_code_flow(authority, client_id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,12 +208,8 @@ pub fn parse_token_response(status: u16, json: &Value) -> TokenResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn device_code_flow(
|
fn device_code_flow(authority: &str, client_id: &str) -> Result<AcquiredToken, GraphError> {
|
||||||
authority: &str,
|
let agent = build_agent();
|
||||||
client_id: &str,
|
|
||||||
allow_invalid_certs: bool,
|
|
||||||
) -> Result<AcquiredToken, GraphError> {
|
|
||||||
let agent = build_agent(allow_invalid_certs);
|
|
||||||
let body = form_encode(&[("client_id", client_id), ("scope", SCOPES)]);
|
let body = form_encode(&[("client_id", client_id), ("scope", SCOPES)]);
|
||||||
let endpoint = device_code_endpoint(authority);
|
let endpoint = device_code_endpoint(authority);
|
||||||
let mut resp = agent
|
let mut resp = agent
|
||||||
@@ -289,9 +285,8 @@ pub fn refresh_access_token(
|
|||||||
authority: &str,
|
authority: &str,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
refresh_token: &str,
|
refresh_token: &str,
|
||||||
allow_invalid_certs: bool,
|
|
||||||
) -> Result<AcquiredToken, GraphError> {
|
) -> Result<AcquiredToken, GraphError> {
|
||||||
let agent = build_agent(allow_invalid_certs);
|
let agent = build_agent();
|
||||||
let body = form_encode(&[
|
let body = form_encode(&[
|
||||||
("client_id", client_id),
|
("client_id", client_id),
|
||||||
("grant_type", "refresh_token"),
|
("grant_type", "refresh_token"),
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||||
|
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
@@ -170,6 +171,7 @@ fn extract_account_id(principal: &Value, name: &str) -> Result<String, Error> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
fn session_with(name: &str, id: &str) -> Session {
|
fn session_with(name: &str, id: &str) -> Session {
|
||||||
let raw = serde_json::json!({
|
let raw = serde_json::json!({
|
||||||
@@ -186,7 +188,7 @@ mod tests {
|
|||||||
HttpClient::new(
|
HttpClient::new(
|
||||||
crate::jmap::http::Auth::Bearer { token: "t".into() },
|
crate::jmap::http::Auth::Bearer { token: "t".into() },
|
||||||
crate::jmap::http::RetryPolicy::new(0),
|
crate::jmap::http::RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-24
@@ -21,7 +21,7 @@ use crate::jmap::inflight::{Permit, Semaphore};
|
|||||||
use crate::jmap::retry::{self, Disposition, RateLimitState};
|
use crate::jmap::retry::{self, Disposition, RateLimitState};
|
||||||
use crate::jmap::session::Limits;
|
use crate::jmap::session::Limits;
|
||||||
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
||||||
use crate::net::{send_body_budget, tls, with_timeouts};
|
use crate::net::{CertOverride, send_body_budget, tls, with_timeouts};
|
||||||
|
|
||||||
const MAX_BODY: u64 = 512 * 1024 * 1024;
|
const MAX_BODY: u64 = 512 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -70,9 +70,10 @@ impl RetryPolicy {
|
|||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
lax_agent: Option<Agent>,
|
||||||
|
certs: CertOverride,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
retry: RetryPolicy,
|
retry: RetryPolicy,
|
||||||
allow_invalid_certs: bool,
|
|
||||||
rate_limit: RateLimitState,
|
rate_limit: RateLimitState,
|
||||||
log_level: AtomicU8,
|
log_level: AtomicU8,
|
||||||
requests_gate: OnceLock<Semaphore>,
|
requests_gate: OnceLock<Semaphore>,
|
||||||
@@ -82,6 +83,17 @@ struct Inner {
|
|||||||
retry_after_sleeps: AtomicU64,
|
retry_after_sleeps: AtomicU64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
/// The agent for `url`: the one that accepts invalid certificates only for
|
||||||
|
/// a host `--allow-invalid-certs` covers, and the verifying one otherwise.
|
||||||
|
fn agent_for(&self, url: &str) -> &Agent {
|
||||||
|
match &self.lax_agent {
|
||||||
|
Some(lax) if self.certs.allows(url) => lax,
|
||||||
|
_ => &self.agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Kind {
|
enum Kind {
|
||||||
Api,
|
Api,
|
||||||
@@ -104,20 +116,25 @@ enum Attempt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HttpClient {
|
impl HttpClient {
|
||||||
pub fn new(auth: Auth, retry: RetryPolicy, allow_invalid_certs: bool) -> Self {
|
pub fn new(auth: Auth, retry: RetryPolicy, certs: CertOverride) -> Self {
|
||||||
let config: Config = with_timeouts!(
|
let build = |accept_invalid: bool| -> Agent {
|
||||||
Config::builder()
|
let config: Config = with_timeouts!(
|
||||||
.http_status_as_error(false)
|
Config::builder()
|
||||||
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
.http_status_as_error(false)
|
||||||
.tls_config(tls(allow_invalid_certs))
|
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
||||||
)
|
.tls_config(tls(accept_invalid))
|
||||||
.build();
|
)
|
||||||
|
.build();
|
||||||
|
config.new_agent()
|
||||||
|
};
|
||||||
|
let lax_agent = certs.is_active().then(|| build(true));
|
||||||
HttpClient {
|
HttpClient {
|
||||||
inner: Arc::new(Inner {
|
inner: Arc::new(Inner {
|
||||||
agent: config.new_agent(),
|
agent: build(false),
|
||||||
|
lax_agent,
|
||||||
|
certs,
|
||||||
auth,
|
auth,
|
||||||
retry,
|
retry,
|
||||||
allow_invalid_certs,
|
|
||||||
rate_limit: RateLimitState::new(),
|
rate_limit: RateLimitState::new(),
|
||||||
log_level: AtomicU8::new(LEVEL_DEFAULT),
|
log_level: AtomicU8::new(LEVEL_DEFAULT),
|
||||||
requests_gate: OnceLock::new(),
|
requests_gate: OnceLock::new(),
|
||||||
@@ -167,10 +184,6 @@ impl HttpClient {
|
|||||||
&self.inner.retry
|
&self.inner.retry
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allow_invalid_certs(&self) -> bool {
|
|
||||||
self.inner.allow_invalid_certs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rate_limit(&self) -> &RateLimitState {
|
pub fn rate_limit(&self) -> &RateLimitState {
|
||||||
&self.inner.rate_limit
|
&self.inner.rate_limit
|
||||||
}
|
}
|
||||||
@@ -381,7 +394,7 @@ impl HttpClient {
|
|||||||
let result = if let Some(payload) = body {
|
let result = if let Some(payload) = body {
|
||||||
let mut req = self
|
let mut req = self
|
||||||
.inner
|
.inner
|
||||||
.agent
|
.agent_for(url)
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("Authorization", auth)
|
.header("Authorization", auth)
|
||||||
.header("Accept", "application/json");
|
.header("Accept", "application/json");
|
||||||
@@ -396,7 +409,7 @@ impl HttpClient {
|
|||||||
.send(payload)
|
.send(payload)
|
||||||
} else {
|
} else {
|
||||||
self.inner
|
self.inner
|
||||||
.agent
|
.agent_for(url)
|
||||||
.get(url)
|
.get(url)
|
||||||
.header("Authorization", auth)
|
.header("Authorization", auth)
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
@@ -644,6 +657,48 @@ pub fn format_retry_wait(d: Duration) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_certificates_are_accepted_only_for_the_named_host() {
|
||||||
|
let client = HttpClient::new(
|
||||||
|
Auth::Bearer {
|
||||||
|
token: "t".to_owned(),
|
||||||
|
},
|
||||||
|
RetryPolicy::new(0),
|
||||||
|
CertOverride::for_url(true, "https://mail.example.test/.well-known/jmap"),
|
||||||
|
);
|
||||||
|
let inner = &client.inner;
|
||||||
|
let lax = inner.lax_agent.as_ref().expect("a relaxed agent exists");
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
inner.agent_for("https://mail.example.test/api"),
|
||||||
|
lax
|
||||||
|
));
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
inner.agent_for("https://files.example.test/upload"),
|
||||||
|
&inner.agent
|
||||||
|
));
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
inner.agent_for("https://login.microsoftonline.com/common/oauth2/v2.0/token"),
|
||||||
|
&inner.agent
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn without_the_flag_there_is_no_relaxed_agent() {
|
||||||
|
let client = HttpClient::new(
|
||||||
|
Auth::Bearer {
|
||||||
|
token: "t".to_owned(),
|
||||||
|
},
|
||||||
|
RetryPolicy::new(0),
|
||||||
|
CertOverride::for_url(false, "https://mail.example.test/"),
|
||||||
|
);
|
||||||
|
assert!(client.inner.lax_agent.is_none());
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
client.inner.agent_for("https://mail.example.test/api"),
|
||||||
|
&client.inner.agent
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_timeout_is_a_retryable_transport_error() {
|
fn every_timeout_is_a_retryable_transport_error() {
|
||||||
@@ -729,7 +784,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
let body = br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"someServerLimit"}"#;
|
let body = br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"someServerLimit"}"#;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -745,7 +800,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
let body = br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"maxSizeRequest"}"#;
|
let body = br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"maxSizeRequest"}"#;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -761,7 +816,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
let body =
|
let body =
|
||||||
br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"maxConcurrentRequests"}"#;
|
br#"{"type":"urn:ietf:params:jmap:error:limit","limit":"maxConcurrentRequests"}"#;
|
||||||
@@ -790,7 +845,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
client.set_limits(&limits_with(10, 4, 4));
|
client.set_limits(&limits_with(10, 4, 4));
|
||||||
let err = client
|
let err = client
|
||||||
@@ -814,7 +869,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
client.set_limits(&limits_with(1024, 4, 4));
|
client.set_limits(&limits_with(1024, 4, 4));
|
||||||
let err = client
|
let err = client
|
||||||
@@ -866,7 +921,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(0),
|
RetryPolicy::new(0),
|
||||||
false,
|
CertOverride::none(),
|
||||||
);
|
);
|
||||||
assert_eq!(client.retries_observed(), 0);
|
assert_eq!(client.retries_observed(), 0);
|
||||||
assert_eq!(client.retry_after_sleeps(), 0);
|
assert_eq!(client.retry_after_sleeps(), 0);
|
||||||
|
|||||||
+2
-1
@@ -786,6 +786,7 @@ fn decode_set(mr: &MethodCall) -> SetOutcome {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::net::CertOverride;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -798,7 +799,7 @@ mod tests {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(max_retries),
|
RetryPolicy::new(max_retries),
|
||||||
false,
|
CertOverride::none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+186
-1
@@ -4,7 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//! Settings every HTTP agent shares: timeouts and TLS.
|
//! Settings every HTTP agent shares: timeouts, TLS, and which hosts, if any,
|
||||||
|
//! may present a certificate that does not verify.
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -72,10 +73,194 @@ pub fn tls(accept_invalid: bool) -> TlsConfig {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hosts that are always verified, whatever `--allow-invalid-certs` says:
|
||||||
|
/// the Microsoft and Google sign-in and cloud endpoints. A certificate that
|
||||||
|
/// fails there is an attack or a broken network, never a self-signed server
|
||||||
|
/// the user meant to trust. Matched as a suffix on a label boundary.
|
||||||
|
const ALWAYS_VERIFY: &[&str] = &[
|
||||||
|
"microsoftonline.com",
|
||||||
|
"microsoftonline.us",
|
||||||
|
"microsoft.com",
|
||||||
|
"microsoft.us",
|
||||||
|
"office365.com",
|
||||||
|
"office.com",
|
||||||
|
"outlook.com",
|
||||||
|
"chinacloudapi.cn",
|
||||||
|
"partner.outlook.cn",
|
||||||
|
"google.com",
|
||||||
|
"googleapis.com",
|
||||||
|
"gmail.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Where `--allow-invalid-certs` applies: the host the user named, or, for
|
||||||
|
/// Exchange Autodiscover without a `--url`, the mailbox's own domain and its
|
||||||
|
/// subdomains. Everything else, including any host a server redirects or
|
||||||
|
/// points to, is verified as usual.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct CertOverride {
|
||||||
|
hosts: Vec<String>,
|
||||||
|
domains: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertOverride {
|
||||||
|
/// Verify everything.
|
||||||
|
pub fn none() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When `enabled`, accept invalid certificates from the host of `url`.
|
||||||
|
pub fn for_url(enabled: bool, url: &str) -> Self {
|
||||||
|
match (enabled, host_of(url)) {
|
||||||
|
(true, Some(host)) if !always_verified(&host) => CertOverride {
|
||||||
|
hosts: vec![host],
|
||||||
|
domains: Vec::new(),
|
||||||
|
},
|
||||||
|
_ => Self::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When `enabled`, accept invalid certificates from `domain` and every
|
||||||
|
/// host under it.
|
||||||
|
pub fn for_domain(enabled: bool, domain: &str) -> Self {
|
||||||
|
let domain = domain.trim_end_matches('.').to_ascii_lowercase();
|
||||||
|
if enabled && !domain.is_empty() && !always_verified(&domain) {
|
||||||
|
CertOverride {
|
||||||
|
hosts: Vec::new(),
|
||||||
|
domains: vec![domain],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same override, narrowed to the host of `url`, if `url` is one this
|
||||||
|
/// override already covers. Used once Autodiscover has found the real
|
||||||
|
/// endpoint.
|
||||||
|
pub fn narrowed_to(&self, url: &str) -> Self {
|
||||||
|
match host_of(url) {
|
||||||
|
Some(host) if self.allows_host(&host) => CertOverride {
|
||||||
|
hosts: vec![host],
|
||||||
|
domains: Vec::new(),
|
||||||
|
},
|
||||||
|
_ => Self::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this override covers anything at all.
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
!self.hosts.is_empty() || !self.domains.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a certificate that does not verify is accepted for `url`.
|
||||||
|
pub fn allows(&self, url: &str) -> bool {
|
||||||
|
host_of(url).is_some_and(|host| self.allows_host(&host))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allows_host(&self, host: &str) -> bool {
|
||||||
|
if always_verified(host) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.hosts.iter().any(|h| h == host)
|
||||||
|
|| self
|
||||||
|
.domains
|
||||||
|
.iter()
|
||||||
|
.any(|d| host == d || host.ends_with(&format!(".{d}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_of(url: &str) -> Option<String> {
|
||||||
|
let parsed = url::Url::parse(url).ok()?;
|
||||||
|
let host = parsed
|
||||||
|
.host_str()?
|
||||||
|
.trim_end_matches('.')
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
Some(
|
||||||
|
host.trim_start_matches('[')
|
||||||
|
.trim_end_matches(']')
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn always_verified(host: &str) -> bool {
|
||||||
|
ALWAYS_VERIFY
|
||||||
|
.iter()
|
||||||
|
.any(|d| host == *d || host.ends_with(&format!(".{d}")))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_flag_covers_nothing() {
|
||||||
|
let o = CertOverride::for_url(false, "https://mail.example.test/jmap");
|
||||||
|
assert!(!o.is_active());
|
||||||
|
assert!(!o.allows("https://mail.example.test/jmap"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn covers_only_the_named_host() {
|
||||||
|
let o = CertOverride::for_url(true, "https://Mail.Example.test:8443/.well-known/jmap");
|
||||||
|
assert!(o.is_active());
|
||||||
|
assert!(o.allows("https://mail.example.test/api"));
|
||||||
|
assert!(o.allows("https://MAIL.example.test:9000/upload"));
|
||||||
|
assert!(!o.allows("https://files.example.test/download"));
|
||||||
|
assert!(!o.allows("https://example.test/"));
|
||||||
|
assert!(!o.allows("https://mail.example.test.evil.test/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sign_in_and_cloud_hosts_are_always_verified() {
|
||||||
|
for url in [
|
||||||
|
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||||
|
"https://graph.microsoft.com/v1.0/me",
|
||||||
|
"https://outlook.office365.com/EWS/Exchange.asmx",
|
||||||
|
"https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml",
|
||||||
|
"https://oauth2.googleapis.com/token",
|
||||||
|
"https://accounts.google.com/o/oauth2/device/code",
|
||||||
|
] {
|
||||||
|
let o = CertOverride::for_url(true, url);
|
||||||
|
assert!(!o.is_active(), "{url}");
|
||||||
|
assert!(!o.allows(url), "{url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_domain_covers_its_subdomains_but_not_look_alikes() {
|
||||||
|
let o = CertOverride::for_domain(true, "Corp.Example.");
|
||||||
|
assert!(o.allows("https://autodiscover.corp.example/autodiscover/autodiscover.xml"));
|
||||||
|
assert!(o.allows("https://corp.example/autodiscover/autodiscover.xml"));
|
||||||
|
assert!(!o.allows("https://notcorp.example/"));
|
||||||
|
assert!(!o.allows("https://corp.example.evil.test/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_domain_override_never_reaches_microsoft() {
|
||||||
|
let o = CertOverride::for_domain(true, "office365.com");
|
||||||
|
assert!(!o.is_active());
|
||||||
|
let corp = CertOverride::for_domain(true, "corp.example");
|
||||||
|
assert!(!corp.allows("https://outlook.office365.com/EWS/Exchange.asmx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn narrowing_keeps_only_a_covered_endpoint() {
|
||||||
|
let o = CertOverride::for_domain(true, "corp.example");
|
||||||
|
let inside = o.narrowed_to("https://mail.corp.example/EWS/Exchange.asmx");
|
||||||
|
assert!(inside.allows("https://mail.corp.example/EWS/Exchange.asmx"));
|
||||||
|
assert!(!inside.allows("https://autodiscover.corp.example/"));
|
||||||
|
let outside = o.narrowed_to("https://outlook.office365.com/EWS/Exchange.asmx");
|
||||||
|
assert!(!outside.is_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ip_literals_are_matched() {
|
||||||
|
let o = CertOverride::for_url(true, "https://[::1]:8443/jmap");
|
||||||
|
assert!(o.allows("https://[::1]:9000/other"));
|
||||||
|
let v4 = CertOverride::for_url(true, "https://192.0.2.10/jmap");
|
||||||
|
assert!(v4.allows("https://192.0.2.10:8443/"));
|
||||||
|
assert!(!v4.allows("https://192.0.2.11/"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn send_budget_grows_with_size() {
|
fn send_budget_grows_with_size() {
|
||||||
assert_eq!(send_body_budget(0), Duration::from_secs(120));
|
assert_eq!(send_body_budget(0), Duration::from_secs(120));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||||
|
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +19,7 @@ use crate::sync::{CommonConfig, RunOutcome, Summary, TypeCounts};
|
|||||||
use super::collections;
|
use super::collections;
|
||||||
use super::items;
|
use super::items;
|
||||||
use super::tree;
|
use super::tree;
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum DavKindArg {
|
pub enum DavKindArg {
|
||||||
@@ -105,7 +107,7 @@ fn run_into(
|
|||||||
let client = DavClient::new(
|
let client = DavClient::new(
|
||||||
config.auth.to_jmap_auth(),
|
config.auth.to_jmap_auth(),
|
||||||
RetryPolicy::new(common.max_retries),
|
RetryPolicy::new(common.max_retries),
|
||||||
common.allow_invalid_certs,
|
CertOverride::for_url(common.allow_invalid_certs, &config.url),
|
||||||
);
|
);
|
||||||
client.set_logger(logger);
|
client.set_logger(logger);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::sync::{CommonConfig, Summary, TypeCounts};
|
|||||||
|
|
||||||
use super::folders::{self, plan_folders};
|
use super::folders::{self, plan_folders};
|
||||||
use super::{calendar, contacts, messages};
|
use super::{calendar, contacts, messages};
|
||||||
|
use crate::net::CertOverride;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum EwsAuth {
|
pub enum EwsAuth {
|
||||||
@@ -45,8 +46,8 @@ pub fn run(common: CommonConfig, config: EwsImportConfig) -> Result<Summary, Err
|
|||||||
let logger = common.logger;
|
let logger = common.logger;
|
||||||
let mut conn = db::init::open(&common.archive)?;
|
let mut conn = db::init::open(&common.archive)?;
|
||||||
|
|
||||||
let (auth, acquired) = resolve_auth(&config.auth, common.allow_invalid_certs)?;
|
let (auth, acquired) = resolve_auth(&config.auth)?;
|
||||||
let discovery = run_autodiscover(&config, &acquired, common.allow_invalid_certs)?;
|
let (discovery, certs) = run_autodiscover(&config, &acquired, common.allow_invalid_certs)?;
|
||||||
if logger.enabled(LEVEL_PROGRESS) {
|
if logger.enabled(LEVEL_PROGRESS) {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"EWS discovery: url={} source={:?}",
|
"EWS discovery: url={} source={:?}",
|
||||||
@@ -77,7 +78,7 @@ pub fn run(common: CommonConfig, config: EwsImportConfig) -> Result<Summary, Err
|
|||||||
let client = EwsClient::new(
|
let client = EwsClient::new(
|
||||||
auth,
|
auth,
|
||||||
RetryPolicy::new(common.max_retries),
|
RetryPolicy::new(common.max_retries),
|
||||||
common.allow_invalid_certs,
|
certs.narrowed_to(&discovery.ews_url),
|
||||||
);
|
);
|
||||||
client.set_logger(logger);
|
client.set_logger(logger);
|
||||||
if matches!(config.mailbox_kind, MailboxKind::PublicFolders) {
|
if matches!(config.mailbox_kind, MailboxKind::PublicFolders) {
|
||||||
@@ -88,13 +89,7 @@ pub fn run(common: CommonConfig, config: EwsImportConfig) -> Result<Summary, Err
|
|||||||
if let EwsAuth::OAuth(OAuthFlow::ClientCredentials { .. }) = &config.auth {
|
if let EwsAuth::OAuth(OAuthFlow::ClientCredentials { .. }) = &config.auth {
|
||||||
client.set_impersonation(Some(mailbox.clone()));
|
client.set_impersonation(Some(mailbox.clone()));
|
||||||
}
|
}
|
||||||
spawn_token_refresher(
|
spawn_token_refresher(&client, &config.auth, &acquired, logger);
|
||||||
&client,
|
|
||||||
&config.auth,
|
|
||||||
&acquired,
|
|
||||||
common.allow_invalid_certs,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
let username = match &config.auth {
|
let username = match &config.auth {
|
||||||
EwsAuth::Basic { user, .. } => user.clone(),
|
EwsAuth::Basic { user, .. } => user.clone(),
|
||||||
@@ -211,10 +206,7 @@ fn run_dry(
|
|||||||
Ok(summary)
|
Ok(summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_auth(
|
fn resolve_auth(auth: &EwsAuth) -> Result<(Auth, Option<AcquiredToken>), Error> {
|
||||||
auth: &EwsAuth,
|
|
||||||
allow_invalid_certs: bool,
|
|
||||||
) -> Result<(Auth, Option<AcquiredToken>), Error> {
|
|
||||||
match auth {
|
match auth {
|
||||||
EwsAuth::Basic { user, password } => Ok((
|
EwsAuth::Basic { user, password } => Ok((
|
||||||
Auth::Basic {
|
Auth::Basic {
|
||||||
@@ -224,12 +216,9 @@ fn resolve_auth(
|
|||||||
None,
|
None,
|
||||||
)),
|
)),
|
||||||
EwsAuth::Bearer { token } => {
|
EwsAuth::Bearer { token } => {
|
||||||
let acq = acquire(
|
let acq = acquire(&OAuthFlow::PreAcquired {
|
||||||
&OAuthFlow::PreAcquired {
|
token: token.clone(),
|
||||||
token: token.clone(),
|
})
|
||||||
},
|
|
||||||
allow_invalid_certs,
|
|
||||||
)
|
|
||||||
.map_err(Error::from)?;
|
.map_err(Error::from)?;
|
||||||
Ok((
|
Ok((
|
||||||
Auth::Bearer {
|
Auth::Bearer {
|
||||||
@@ -239,7 +228,7 @@ fn resolve_auth(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
EwsAuth::OAuth(flow) => {
|
EwsAuth::OAuth(flow) => {
|
||||||
let acq = acquire(flow, allow_invalid_certs).map_err(Error::from)?;
|
let acq = acquire(flow).map_err(Error::from)?;
|
||||||
Ok((
|
Ok((
|
||||||
Auth::Bearer {
|
Auth::Bearer {
|
||||||
token: acq.access_token.clone(),
|
token: acq.access_token.clone(),
|
||||||
@@ -254,19 +243,36 @@ fn run_autodiscover(
|
|||||||
config: &EwsImportConfig,
|
config: &EwsImportConfig,
|
||||||
acquired: &Option<AcquiredToken>,
|
acquired: &Option<AcquiredToken>,
|
||||||
allow_invalid_certs: bool,
|
allow_invalid_certs: bool,
|
||||||
) -> Result<DiscoveryResult, Error> {
|
) -> Result<(DiscoveryResult, CertOverride), Error> {
|
||||||
let email = config
|
let email = config
|
||||||
.mailbox
|
.mailbox
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| acquired.as_ref().and_then(|a| a.upn.clone()));
|
.or_else(|| acquired.as_ref().and_then(|a| a.upn.clone()));
|
||||||
let result = discover(
|
let certs =
|
||||||
config.url.as_deref(),
|
autodiscover_cert_override(config.url.as_deref(), email.as_deref(), allow_invalid_certs);
|
||||||
email.as_deref(),
|
let result =
|
||||||
None,
|
discover(config.url.as_deref(), email.as_deref(), None, &certs).map_err(Error::from)?;
|
||||||
allow_invalid_certs,
|
Ok((result, certs))
|
||||||
)
|
}
|
||||||
.map_err(Error::from)?;
|
|
||||||
Ok(result)
|
/// Where `--allow-invalid-certs` applies for an EWS import: the host of
|
||||||
|
/// `--url` when one is given, and otherwise the mailbox's own domain, which is
|
||||||
|
/// where on-premises Autodiscover looks. Microsoft's hosts are never covered.
|
||||||
|
fn autodiscover_cert_override(
|
||||||
|
url: Option<&str>,
|
||||||
|
email: Option<&str>,
|
||||||
|
enabled: bool,
|
||||||
|
) -> CertOverride {
|
||||||
|
match (
|
||||||
|
url,
|
||||||
|
email
|
||||||
|
.and_then(|e| e.rsplit_once('@'))
|
||||||
|
.map(|(_, domain)| domain),
|
||||||
|
) {
|
||||||
|
(Some(url), _) => CertOverride::for_url(enabled, url),
|
||||||
|
(None, Some(domain)) => CertOverride::for_domain(enabled, domain),
|
||||||
|
(None, None) => CertOverride::none(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_mailbox(
|
fn resolve_mailbox(
|
||||||
@@ -370,7 +376,6 @@ fn spawn_token_refresher(
|
|||||||
client: &EwsClient,
|
client: &EwsClient,
|
||||||
auth: &EwsAuth,
|
auth: &EwsAuth,
|
||||||
initial: &Option<AcquiredToken>,
|
initial: &Option<AcquiredToken>,
|
||||||
allow_invalid_certs: bool,
|
|
||||||
logger: crate::logging::Logger,
|
logger: crate::logging::Logger,
|
||||||
) {
|
) {
|
||||||
let flow = match auth {
|
let flow = match auth {
|
||||||
@@ -402,14 +407,9 @@ fn spawn_token_refresher(
|
|||||||
let result = if let (Some(rt), OAuthFlow::DeviceCode { tenant, client_id }) =
|
let result = if let (Some(rt), OAuthFlow::DeviceCode { tenant, client_id }) =
|
||||||
(refresh_token.as_deref(), &flow)
|
(refresh_token.as_deref(), &flow)
|
||||||
{
|
{
|
||||||
crate::exchange_ews::oauth::refresh_with_token(
|
crate::exchange_ews::oauth::refresh_with_token(tenant, client_id, rt)
|
||||||
tenant,
|
|
||||||
client_id,
|
|
||||||
rt,
|
|
||||||
allow_invalid_certs,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
crate::exchange_ews::oauth::acquire(&flow, allow_invalid_certs)
|
crate::exchange_ews::oauth::acquire(&flow)
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(tok) => {
|
Ok(tok) => {
|
||||||
@@ -451,6 +451,26 @@ fn run_gc(conn: &Connection) -> Result<(), Error> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cert_override_follows_url_then_mailbox_domain() {
|
||||||
|
let by_url = autodiscover_cert_override(
|
||||||
|
Some("https://mail.corp.example/EWS/Exchange.asmx"),
|
||||||
|
Some("[email protected]"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert!(by_url.allows("https://mail.corp.example/EWS/Exchange.asmx"));
|
||||||
|
assert!(!by_url.allows("https://autodiscover.corp.example/"));
|
||||||
|
|
||||||
|
let by_domain = autodiscover_cert_override(None, Some("[email protected]"), true);
|
||||||
|
assert!(
|
||||||
|
by_domain.allows("https://autodiscover.corp.example/autodiscover/autodiscover.xml")
|
||||||
|
);
|
||||||
|
assert!(!by_domain.allows("https://outlook.office365.com/EWS/Exchange.asmx"));
|
||||||
|
|
||||||
|
assert!(!autodiscover_cert_override(None, Some("[email protected]"), false).is_active());
|
||||||
|
assert!(!autodiscover_cert_override(None, None, true).is_active());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn synthetic_account_id_uses_smtp_for_primary() {
|
fn synthetic_account_id_uses_smtp_for_primary() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::exchange_graph::oauth::{
|
|||||||
use crate::exchange_graph::types::{EventBodyFormat, MailboxKind, Surfaces, synthetic_account_id};
|
use crate::exchange_graph::types::{EventBodyFormat, MailboxKind, Surfaces, synthetic_account_id};
|
||||||
use crate::jmap::http::RetryPolicy;
|
use crate::jmap::http::RetryPolicy;
|
||||||
use crate::logging::LEVEL_DEFAULT;
|
use crate::logging::LEVEL_DEFAULT;
|
||||||
|
use crate::net::CertOverride;
|
||||||
use crate::sync::{CommonConfig, Summary, TypeCounts};
|
use crate::sync::{CommonConfig, Summary, TypeCounts};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -68,11 +69,11 @@ pub fn run(common: CommonConfig, config: GraphImportConfig) -> Result<Summary, E
|
|||||||
let logger = common.logger;
|
let logger = common.logger;
|
||||||
let mut conn = db::init::open(&common.archive)?;
|
let mut conn = db::init::open(&common.archive)?;
|
||||||
|
|
||||||
let acquired = acquire_with_flow(&config.auth, common.allow_invalid_certs)?;
|
let acquired = acquire_with_flow(&config.auth)?;
|
||||||
let client = GraphClient::new(
|
let client = GraphClient::new(
|
||||||
acquired.access_token.clone(),
|
acquired.access_token.clone(),
|
||||||
RetryPolicy::new(common.max_retries),
|
RetryPolicy::new(common.max_retries),
|
||||||
common.allow_invalid_certs,
|
CertOverride::for_url(common.allow_invalid_certs, &config.api_base),
|
||||||
);
|
);
|
||||||
client.set_logger(logger);
|
client.set_logger(logger);
|
||||||
|
|
||||||
@@ -121,13 +122,7 @@ pub fn run(common: CommonConfig, config: GraphImportConfig) -> Result<Summary, E
|
|||||||
&principal.user_principal_name,
|
&principal.user_principal_name,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let _refresher = spawn_token_refresher(
|
let _refresher = spawn_token_refresher(&client, &config.auth, &acquired, logger);
|
||||||
&client,
|
|
||||||
&config.auth,
|
|
||||||
&acquired,
|
|
||||||
common.allow_invalid_certs,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut summary = Summary::default();
|
let mut summary = Summary::default();
|
||||||
let mut mailbox_counts = TypeCounts::default();
|
let mut mailbox_counts = TypeCounts::default();
|
||||||
@@ -282,7 +277,7 @@ pub fn enumerate_mail_folders(
|
|||||||
Ok(all)
|
Ok(all)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acquire_with_flow(auth: &GraphAuth, allow_invalid_certs: bool) -> Result<AcquiredToken, Error> {
|
fn acquire_with_flow(auth: &GraphAuth) -> Result<AcquiredToken, Error> {
|
||||||
let flow = match auth {
|
let flow = match auth {
|
||||||
GraphAuth::PreAcquired { token } => OAuthFlow::PreAcquired {
|
GraphAuth::PreAcquired { token } => OAuthFlow::PreAcquired {
|
||||||
token: token.clone(),
|
token: token.clone(),
|
||||||
@@ -295,7 +290,7 @@ fn acquire_with_flow(auth: &GraphAuth, allow_invalid_certs: bool) -> Result<Acqu
|
|||||||
client_id: client_id.clone(),
|
client_id: client_id.clone(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
acquire(&flow, allow_invalid_certs).map_err(Error::from)
|
acquire(&flow).map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_endpoints(config: &GraphImportConfig, client: &GraphClient) -> Result<Endpoints, Error> {
|
fn resolve_endpoints(config: &GraphImportConfig, client: &GraphClient) -> Result<Endpoints, Error> {
|
||||||
@@ -379,7 +374,6 @@ fn spawn_token_refresher(
|
|||||||
client: &GraphClient,
|
client: &GraphClient,
|
||||||
auth: &GraphAuth,
|
auth: &GraphAuth,
|
||||||
initial: &AcquiredToken,
|
initial: &AcquiredToken,
|
||||||
allow_invalid_certs: bool,
|
|
||||||
logger: crate::logging::Logger,
|
logger: crate::logging::Logger,
|
||||||
) -> Option<TokenRefresher> {
|
) -> Option<TokenRefresher> {
|
||||||
let (authority, client_id) = match auth {
|
let (authority, client_id) = match auth {
|
||||||
@@ -416,12 +410,7 @@ fn spawn_token_refresher(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match refresh_access_token(
|
match refresh_access_token(&authority, &client_id, &refresh_token) {
|
||||||
&authority,
|
|
||||||
&client_id,
|
|
||||||
&refresh_token,
|
|
||||||
allow_invalid_certs,
|
|
||||||
) {
|
|
||||||
Ok(tok) => {
|
Ok(tok) => {
|
||||||
client.set_bearer(tok.access_token.clone());
|
client.set_bearer(tok.access_token.clone());
|
||||||
if let Some(new_refresh) = tok.refresh_token {
|
if let Some(new_refresh) = tok.refresh_token {
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||||
|
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
@@ -41,6 +42,7 @@ pub(crate) fn table_name(ty: ObjectType) -> &'static str {
|
|||||||
use crate::jmap::account::AccountSelector;
|
use crate::jmap::account::AccountSelector;
|
||||||
use crate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
use crate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||||
use crate::logging::Logger;
|
use crate::logging::Logger;
|
||||||
|
use crate::net::CertOverride;
|
||||||
use crate::types::ObjectType;
|
use crate::types::ObjectType;
|
||||||
|
|
||||||
pub struct CommonConfig {
|
pub struct CommonConfig {
|
||||||
@@ -134,7 +136,7 @@ impl Context {
|
|||||||
let client = HttpClient::new(
|
let client = HttpClient::new(
|
||||||
connect.auth.clone(),
|
connect.auth.clone(),
|
||||||
RetryPolicy::new(common.max_retries),
|
RetryPolicy::new(common.max_retries),
|
||||||
common.allow_invalid_certs,
|
CertOverride::for_url(common.allow_invalid_certs, &connect.url),
|
||||||
);
|
);
|
||||||
Ok(Context {
|
Ok(Context {
|
||||||
conn,
|
conn,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod seeder;
|
|||||||
use inbuxa_migrate::jmap::account::{self, AccountSelector};
|
use inbuxa_migrate::jmap::account::{self, AccountSelector};
|
||||||
use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||||
use inbuxa_migrate::jmap::session::Session;
|
use inbuxa_migrate::jmap::session::Session;
|
||||||
|
use inbuxa_migrate::net::CertOverride;
|
||||||
use integration::stalwart::shared as shared_stalwart;
|
use integration::stalwart::shared as shared_stalwart;
|
||||||
|
|
||||||
fn admin_client() -> HttpClient {
|
fn admin_client() -> HttpClient {
|
||||||
@@ -20,7 +21,7 @@ fn admin_client() -> HttpClient {
|
|||||||
password: seeder::ADMIN_PASSWORD.into(),
|
password: seeder::ADMIN_PASSWORD.into(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(5),
|
RetryPolicy::new(5),
|
||||||
true,
|
CertOverride::for_url(true, shared_stalwart().base_url()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@ use inbuxa_migrate::dav::parse::{parse_multistatus, strip_ascii_control_chars};
|
|||||||
use inbuxa_migrate::dav::xml;
|
use inbuxa_migrate::dav::xml;
|
||||||
use inbuxa_migrate::jmap::error::JmapError;
|
use inbuxa_migrate::jmap::error::JmapError;
|
||||||
use inbuxa_migrate::jmap::http::{Auth, RetryPolicy};
|
use inbuxa_migrate::jmap::http::{Auth, RetryPolicy};
|
||||||
|
use inbuxa_migrate::net::CertOverride;
|
||||||
|
|
||||||
fn client(retries: u32) -> DavClient {
|
fn client(retries: u32) -> DavClient {
|
||||||
DavClient::new(
|
DavClient::new(
|
||||||
@@ -21,7 +22,7 @@ fn client(retries: u32) -> DavClient {
|
|||||||
password: "p".into(),
|
password: "p".into(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(retries),
|
RetryPolicy::new(retries),
|
||||||
false,
|
CertOverride::none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use inbuxa_migrate::exchange_ews::xml::{
|
|||||||
get_item_body, sync_folder_items_body,
|
get_item_body, sync_folder_items_body,
|
||||||
};
|
};
|
||||||
use inbuxa_migrate::jmap::http::{Auth, RetryPolicy};
|
use inbuxa_migrate::jmap::http::{Auth, RetryPolicy};
|
||||||
|
use inbuxa_migrate::net::CertOverride;
|
||||||
use mockito::Matcher;
|
use mockito::Matcher;
|
||||||
|
|
||||||
const TXT_XML: &str = "text/xml; charset=utf-8";
|
const TXT_XML: &str = "text/xml; charset=utf-8";
|
||||||
@@ -33,7 +34,7 @@ fn client(retries: u32) -> EwsClient {
|
|||||||
token: "t".to_owned(),
|
token: "t".to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(retries),
|
RetryPolicy::new(retries),
|
||||||
false,
|
CertOverride::none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ fn autodiscover_v2_returns_global_endpoint() {
|
|||||||
let _ = server;
|
let _ = server;
|
||||||
let url = "https://outlook.office365.com/EWS/Exchange.asmx";
|
let url = "https://outlook.office365.com/EWS/Exchange.asmx";
|
||||||
assert!(inbuxa_migrate::exchange_ews::autodiscover::is_fully_qualified_ews_url(url));
|
assert!(inbuxa_migrate::exchange_ews::autodiscover::is_fully_qualified_ews_url(url));
|
||||||
let r = discover(Some(url), None, None, false).unwrap();
|
let r = discover(Some(url), None, None, &CertOverride::none()).unwrap();
|
||||||
assert_eq!(r.source, DiscoverySource::SuppliedUrl);
|
assert_eq!(r.source, DiscoverySource::SuppliedUrl);
|
||||||
assert_eq!(r.ews_url, url);
|
assert_eq!(r.ews_url, url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use inbuxa_migrate::exchange_graph::recurrence::convert_patterned_recurrence;
|
|||||||
use inbuxa_migrate::exchange_graph::retry::{HttpClass, classify_http_status};
|
use inbuxa_migrate::exchange_graph::retry::{HttpClass, classify_http_status};
|
||||||
use inbuxa_migrate::exchange_graph::types::Surfaces;
|
use inbuxa_migrate::exchange_graph::types::Surfaces;
|
||||||
use inbuxa_migrate::jmap::http::RetryPolicy;
|
use inbuxa_migrate::jmap::http::RetryPolicy;
|
||||||
|
use inbuxa_migrate::net::CertOverride;
|
||||||
use mockito::{Matcher, Server};
|
use mockito::{Matcher, Server};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -28,7 +29,11 @@ static INIT: Once = Once::new();
|
|||||||
|
|
||||||
fn client_with_retries(retries: u32) -> GraphClient {
|
fn client_with_retries(retries: u32) -> GraphClient {
|
||||||
INIT.call_once(|| {});
|
INIT.call_once(|| {});
|
||||||
GraphClient::new("BEARER".to_owned(), RetryPolicy::new(retries), false)
|
GraphClient::new(
|
||||||
|
"BEARER".to_owned(),
|
||||||
|
RetryPolicy::new(retries),
|
||||||
|
CertOverride::none(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url_message_collection(server_url: &str, folder: &str, top: usize) -> String {
|
fn url_message_collection(server_url: &str, folder: &str, top: usize) -> String {
|
||||||
@@ -1681,7 +1686,11 @@ fn graph_client_retries_after_401_when_bearer_is_swapped() {
|
|||||||
.expect(1)
|
.expect(1)
|
||||||
.create();
|
.create();
|
||||||
let base = server.url();
|
let base = server.url();
|
||||||
let client = GraphClient::new("EXPIRED".to_owned(), RetryPolicy::new(0), false);
|
let client = GraphClient::new(
|
||||||
|
"EXPIRED".to_owned(),
|
||||||
|
RetryPolicy::new(0),
|
||||||
|
CertOverride::none(),
|
||||||
|
);
|
||||||
let url = format!("{base}/me");
|
let url = format!("{base}/me");
|
||||||
let err = client.get(&url, Accept::Json).unwrap_err();
|
let err = client.get(&url, Accept::Json).unwrap_err();
|
||||||
assert!(matches!(err, GraphError::Auth(_)));
|
assert!(matches!(err, GraphError::Auth(_)));
|
||||||
|
|||||||
+2
-1
@@ -17,6 +17,7 @@ use inbuxa_migrate::jmap::session::{Limits, Session};
|
|||||||
use inbuxa_migrate::jmap::wire::JmapId;
|
use inbuxa_migrate::jmap::wire::JmapId;
|
||||||
use inbuxa_migrate::jmap::wire::identity::Identity;
|
use inbuxa_migrate::jmap::wire::identity::Identity;
|
||||||
use inbuxa_migrate::jmap::wire::mailbox::Mailbox;
|
use inbuxa_migrate::jmap::wire::mailbox::Mailbox;
|
||||||
|
use inbuxa_migrate::net::CertOverride;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
fn client(retries: u32) -> HttpClient {
|
fn client(retries: u32) -> HttpClient {
|
||||||
@@ -26,7 +27,7 @@ fn client(retries: u32) -> HttpClient {
|
|||||||
password: "p".into(),
|
password: "p".into(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(retries),
|
RetryPolicy::new(retries),
|
||||||
false,
|
CertOverride::none(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-5
@@ -16,6 +16,7 @@ use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
|||||||
use inbuxa_migrate::jmap::request::Request;
|
use inbuxa_migrate::jmap::request::Request;
|
||||||
use inbuxa_migrate::jmap::session::Session;
|
use inbuxa_migrate::jmap::session::Session;
|
||||||
use inbuxa_migrate::logging::Logger;
|
use inbuxa_migrate::logging::Logger;
|
||||||
|
use inbuxa_migrate::net::CertOverride;
|
||||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||||
use integration::stalwart::shared as shared_stalwart;
|
use integration::stalwart::shared as shared_stalwart;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
@@ -785,7 +786,11 @@ fn export_inlines_contact_and_event_blobs_instead_of_blob_ids() {
|
|||||||
assert_eq!(counts.failed, 0, "{name} had no failures: {counts:?}");
|
assert_eq!(counts.failed, 0, "{name} had no failures: {counts:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = HttpClient::new(basic("test6"), RetryPolicy::new(5), true);
|
let client = HttpClient::new(
|
||||||
|
basic("test6"),
|
||||||
|
RetryPolicy::new(5),
|
||||||
|
CertOverride::for_url(true, base_url()),
|
||||||
|
);
|
||||||
let session = Session::discover(&client, base_url()).expect("discover target session");
|
let session = Session::discover(&client, base_url()).expect("discover target session");
|
||||||
let api = session.api_url.clone();
|
let api = session.api_url.clone();
|
||||||
|
|
||||||
@@ -1047,7 +1052,11 @@ fn live_burst_exceeds_concurrent_requests_and_recovers() {
|
|||||||
let fx = seeder::provision(base_url()).expect("provision");
|
let fx = seeder::provision(base_url()).expect("provision");
|
||||||
let acc = fx.account("test1").expect("test1");
|
let acc = fx.account("test1").expect("test1");
|
||||||
|
|
||||||
let client = HttpClient::new(basic("test1"), RetryPolicy::new(20), true);
|
let client = HttpClient::new(
|
||||||
|
basic("test1"),
|
||||||
|
RetryPolicy::new(20),
|
||||||
|
CertOverride::for_url(true, base_url()),
|
||||||
|
);
|
||||||
let session = Session::discover(&client, base_url()).expect("discover session");
|
let session = Session::discover(&client, base_url()).expect("discover session");
|
||||||
let server_limits = session.core_limits().expect("core limits");
|
let server_limits = session.core_limits().expect("core limits");
|
||||||
|
|
||||||
@@ -1148,7 +1157,7 @@ impl JmapSettingsGuard {
|
|||||||
password: seeder::ADMIN_PASSWORD.to_owned(),
|
password: seeder::ADMIN_PASSWORD.to_owned(),
|
||||||
},
|
},
|
||||||
RetryPolicy::new(5),
|
RetryPolicy::new(5),
|
||||||
true,
|
CertOverride::for_url(true, base_url()),
|
||||||
);
|
);
|
||||||
let session = Session::discover(&admin, base_url()).expect("admin discover");
|
let session = Session::discover(&admin, base_url()).expect("admin discover");
|
||||||
let admin_account = session
|
let admin_account = session
|
||||||
@@ -1269,7 +1278,11 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
|||||||
);
|
);
|
||||||
let _ttl_guard = JmapSettingsGuard::override_settings(updates);
|
let _ttl_guard = JmapSettingsGuard::override_settings(updates);
|
||||||
|
|
||||||
let client = HttpClient::new(basic("test1"), RetryPolicy::new(20), true);
|
let client = HttpClient::new(
|
||||||
|
basic("test1"),
|
||||||
|
RetryPolicy::new(20),
|
||||||
|
CertOverride::for_url(true, base_url()),
|
||||||
|
);
|
||||||
let session = Session::discover(&client, base_url()).expect("discover session");
|
let session = Session::discover(&client, base_url()).expect("discover session");
|
||||||
let limits = session.core_limits().expect("core limits");
|
let limits = session.core_limits().expect("core limits");
|
||||||
client.set_limits(&limits);
|
client.set_limits(&limits);
|
||||||
@@ -1346,7 +1359,11 @@ fn import_delta_propagates_email_keyword_change_via_changes() {
|
|||||||
.expect("an unflagged email exists in the archive")
|
.expect("an unflagged email exists in the archive")
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = HttpClient::new(basic("test1"), RetryPolicy::new(5), true);
|
let client = HttpClient::new(
|
||||||
|
basic("test1"),
|
||||||
|
RetryPolicy::new(5),
|
||||||
|
CertOverride::for_url(true, base_url()),
|
||||||
|
);
|
||||||
let session = Session::discover(&client, base_url()).expect("session discovered");
|
let session = Session::discover(&client, base_url()).expect("session discovered");
|
||||||
let account = account::resolve(
|
let account = account::resolve(
|
||||||
&AccountSelector::Id(acc.account_id.clone()),
|
&AccountSelector::Id(acc.account_id.clone()),
|
||||||
|
|||||||
Reference in New Issue
Block a user