mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: enhance autoconfig detection
This commit is contained in:
@@ -45,6 +45,10 @@ pub struct IncomingServer {
|
||||
#[serde(rename = "socketType")]
|
||||
pub socket_type: String,
|
||||
pub username: String,
|
||||
/// Authentication method from the XML, e.g. "OAuth2", "password-cleartext",
|
||||
/// "password-encrypted", "GSSAPI", "NTLM". Absent in DNS SRV fallback.
|
||||
#[serde(default)]
|
||||
pub authentication: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
|
||||
@@ -137,6 +141,7 @@ async fn lookup_srv(domain: &str) -> Option<MailConfig> {
|
||||
port: imap_port,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
}],
|
||||
outgoing: vec![OutgoingServer {
|
||||
protocol: "smtp".to_string(),
|
||||
@@ -153,30 +158,37 @@ async fn lookup_srv(domain: &str) -> Option<MailConfig> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Discover mail server configuration for a domain using the Thunderbird
|
||||
/// autoconfig protocol (ISPDB) and DNS SRV fallback.
|
||||
/// autoconfig protocol (ISPDB), DNS SRV, MX fallback, and finally guessing.
|
||||
///
|
||||
/// Probe order:
|
||||
/// 1. `https://autoconfig.{domain}/mail/config-v1.1.xml`
|
||||
/// 2. `https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml`
|
||||
/// 3. DNS SRV records (`_imaps._tcp` / `_submission._tcp`)
|
||||
/// 4. Thunderbird central ISPDB (`https://autoconfig.thunderbird.net/v1.1/{domain}`)
|
||||
/// 2. `http://autoconfig.{domain}/mail/config-v1.1.xml`
|
||||
/// 3. `https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml`
|
||||
/// 4. `http://{domain}/.well-known/autoconfig/mail/config-v1.1.xml`
|
||||
/// 5. DNS SRV records (`_imaps._tcp` / `_submission._tcp`)
|
||||
/// 6. Thunderbird central ISPDB (`https://autoconfig.thunderbird.net/v1.1/{domain}`)
|
||||
/// 7. MX lookup → ISPDB for MX domain
|
||||
/// 8. MX lookup → ISP autoconfig for MX domain
|
||||
/// 9. GuessConfig — probe common hostnames + ports
|
||||
pub async fn fetch(domain: &str) -> BichonResult<MailConfig> {
|
||||
let client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
|
||||
// 1. Try autoconfig subdomain
|
||||
if let Some(config) = fetch_xml(
|
||||
&client,
|
||||
&format!("https://autoconfig.{domain}/mail/config-v1.1.xml"),
|
||||
)
|
||||
.await
|
||||
// ── ISP autoconfig (HTTPS, then HTTP) ──────────────────────────
|
||||
if let Some(config) =
|
||||
fetch_xml(&client, &format!("https://autoconfig.{domain}/mail/config-v1.1.xml")).await
|
||||
{
|
||||
return Ok(config);
|
||||
}
|
||||
if let Some(config) =
|
||||
fetch_xml(&client, &format!("http://autoconfig.{domain}/mail/config-v1.1.xml")).await
|
||||
{
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// 2. Try well-known path
|
||||
// ── Well-known path (HTTPS, then HTTP) ─────────────────────────
|
||||
if let Some(config) = fetch_xml(
|
||||
&client,
|
||||
&format!("https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml"),
|
||||
@@ -185,28 +197,100 @@ pub async fn fetch(domain: &str) -> BichonResult<MailConfig> {
|
||||
{
|
||||
return Ok(config);
|
||||
}
|
||||
if let Some(config) = fetch_xml(
|
||||
&client,
|
||||
&format!("http://{domain}/.well-known/autoconfig/mail/config-v1.1.xml"),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// 3. Try DNS SRV records
|
||||
// ── DNS SRV records ────────────────────────────────────────────
|
||||
if let Some(config) = lookup_srv(domain).await {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// 4. Fall back to Thunderbird central database
|
||||
if let Some(config) = fetch_xml(
|
||||
&client,
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{domain}"),
|
||||
)
|
||||
.await
|
||||
// ── Thunderbird central ISPDB ──────────────────────────────────
|
||||
if let Some(config) =
|
||||
fetch_xml(&client, &format!("https://autoconfig.thunderbird.net/v1.1/{domain}")).await
|
||||
{
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// ── MX fallback ────────────────────────────────────────────────
|
||||
if let Some(config) = fetch_for_mx(&client, domain).await {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// ── GuessConfig ────────────────────────────────────────────────
|
||||
if let Some(config) = crate::autoconfig::guess::guess_config(domain).await {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
Err(raise_error!(
|
||||
format!("No autoconfig found for domain: {domain}"),
|
||||
ErrorCode::InternalError
|
||||
))
|
||||
}
|
||||
|
||||
/// DNS MX lookup → retry ISPDB and ISP autoconfig for the MX domain.
|
||||
///
|
||||
/// Many self-hosted domains have their MX pointed at Google, Microsoft, etc.
|
||||
/// The MX domain's ISPDB entry covers the original domain.
|
||||
async fn fetch_for_mx(client: &Client, domain: &str) -> Option<MailConfig> {
|
||||
let mx_domain = lookup_mx_domain(domain).await?;
|
||||
if mx_domain == domain.to_ascii_lowercase() {
|
||||
return None; // same domain, already tried above
|
||||
}
|
||||
|
||||
// Try ISPDB for the MX domain
|
||||
if let Some(config) =
|
||||
fetch_xml(client, &format!("https://autoconfig.thunderbird.net/v1.1/{mx_domain}")).await
|
||||
{
|
||||
return Some(config);
|
||||
}
|
||||
|
||||
// Try ISP autoconfig for the MX domain (HTTPS then HTTP)
|
||||
if let Some(config) =
|
||||
fetch_xml(client, &format!("https://autoconfig.{mx_domain}/mail/config-v1.1.xml")).await
|
||||
{
|
||||
return Some(config);
|
||||
}
|
||||
if let Some(config) =
|
||||
fetch_xml(client, &format!("http://autoconfig.{mx_domain}/mail/config-v1.1.xml")).await
|
||||
{
|
||||
return Some(config);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// DNS MX lookup → extract the second-level domain of the first MX hostname.
|
||||
async fn lookup_mx_domain(domain: &str) -> Option<String> {
|
||||
let resolver = TokioResolver::builder(TokioConnectionProvider::default())
|
||||
.ok()?
|
||||
.build();
|
||||
let lookup = resolver.mx_lookup(domain).await.ok()?;
|
||||
let record = lookup.iter().next()?;
|
||||
let mx_host = record.to_string().trim_end_matches('.').to_string();
|
||||
|
||||
// Extract a reasonable base domain from the MX hostname.
|
||||
// E.g., "aspmx.l.google.com" → "google.com"
|
||||
// "company.mail.protection.outlook.com" → "outlook.com"
|
||||
extract_base_domain(&mx_host)
|
||||
}
|
||||
|
||||
/// Extract the top two labels from a hostname as a rough base domain.
|
||||
fn extract_base_domain(host: &str) -> Option<String> {
|
||||
let parts: Vec<&str> = host.split('.').collect();
|
||||
if parts.len() >= 2 {
|
||||
Some(parts[parts.len() - 2..].join("."))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::account::entity::Encryption;
|
||||
use crate::autoconfig::client::{IncomingServer, MailConfig};
|
||||
use crate::imap::client::Client;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// A single host:port:encryption combination to probe.
|
||||
struct Guess {
|
||||
hostname: String,
|
||||
port: u16,
|
||||
encryption: Encryption,
|
||||
socket_type: &'static str,
|
||||
}
|
||||
|
||||
/// Generate candidates in the same order Thunderbird uses:
|
||||
/// 1. imap.{domain} — most common
|
||||
/// 2. mail.{domain} — fallback
|
||||
/// 3. {domain} — bare domain (rare)
|
||||
fn make_guesses(domain: &str) -> Vec<Guess> {
|
||||
let hosts = [
|
||||
format!("imap.{domain}"),
|
||||
format!("mail.{domain}"),
|
||||
domain.to_string(),
|
||||
];
|
||||
|
||||
let mut guesses = Vec::with_capacity(hosts.len() * 2);
|
||||
for host in &hosts {
|
||||
guesses.push(Guess {
|
||||
hostname: host.clone(),
|
||||
port: 993,
|
||||
encryption: Encryption::Ssl,
|
||||
socket_type: "SSL",
|
||||
});
|
||||
guesses.push(Guess {
|
||||
hostname: host.clone(),
|
||||
port: 143,
|
||||
encryption: Encryption::StartTls,
|
||||
socket_type: "STARTTLS",
|
||||
});
|
||||
}
|
||||
guesses
|
||||
}
|
||||
|
||||
/// Try to open a connection, read the IMAP banner, and close.
|
||||
/// Returns `true` if the server responds with an IMAP greeting.
|
||||
async fn probe(hostname: &str, port: u16, encryption: &Encryption) -> bool {
|
||||
match Client::connection(hostname, encryption, port, None, true).await {
|
||||
Ok(_) => {
|
||||
debug!("GuessConfig probe succeeded: {hostname}:{port} ({encryption:?})");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("GuessConfig probe failed for {hostname}:{port}: {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thunderbird-style guessing: try common hostnames and ports, probing
|
||||
/// each with a real TCP connection.
|
||||
///
|
||||
/// Returns the first working `MailConfig`, or `None` if nothing works.
|
||||
pub async fn guess_config(domain: &str) -> Option<MailConfig> {
|
||||
let guesses = make_guesses(domain);
|
||||
info!("GuessConfig: trying {} candidates for {domain}", guesses.len());
|
||||
|
||||
for g in &guesses {
|
||||
if probe(&g.hostname, g.port, &g.encryption).await {
|
||||
info!(
|
||||
"GuessConfig: found working IMAP at {}:{} ({})",
|
||||
g.hostname, g.port, g.socket_type
|
||||
);
|
||||
return Some(MailConfig {
|
||||
incoming: vec![IncomingServer {
|
||||
protocol: "imap".to_string(),
|
||||
hostname: g.hostname.clone(),
|
||||
port: g.port,
|
||||
socket_type: g.socket_type.to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
use crate::account::entity::Encryption;
|
||||
use crate::autoconfig::client::{self, MailConfig};
|
||||
use crate::autoconfig::entity::{MailServerConfig, ServerConfig};
|
||||
use crate::autoconfig::oauth2_providers::lookup_oauth2;
|
||||
use crate::autoconfig::CachedMailSettings;
|
||||
use crate::error::code::ErrorCode;
|
||||
use crate::error::BichonResult;
|
||||
@@ -54,9 +55,17 @@ pub(crate) fn mail_config_to_server_config(config: &MailConfig) -> Option<MailSe
|
||||
}
|
||||
};
|
||||
|
||||
// Detect OAuth2 support: the XML <authentication> field and a known
|
||||
// hostname → issuer mapping determine whether the provider supports OAuth2.
|
||||
let oauth2 = if imap.authentication.eq_ignore_ascii_case("OAuth2") {
|
||||
lookup_oauth2(&imap.hostname)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(MailServerConfig {
|
||||
imap: ServerConfig::new(imap.hostname.clone(), port, encryption),
|
||||
oauth2: None,
|
||||
oauth2,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod client;
|
||||
pub mod entity;
|
||||
pub mod guess;
|
||||
pub mod load;
|
||||
mod oauth2_providers;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::autoconfig::entity::OAuth2Config;
|
||||
|
||||
/// Per-provider OAuth2 metadata, mirroring Thunderbird's `OAuth2Providers.sys.mjs`.
|
||||
///
|
||||
/// Each entry maps one or more IMAP hostname suffixes to a well-known OIDC issuer
|
||||
/// and the IMAP-specific OAuth2 scopes.
|
||||
struct Provider {
|
||||
/// Suffixes matched case-insensitively against the end of the IMAP hostname.
|
||||
host_suffixes: &'static [&'static str],
|
||||
/// The OIDC issuer URL used by the provider.
|
||||
issuer: &'static str,
|
||||
/// OAuth2 scope(s) required for IMAP access.
|
||||
scopes: &'static [&'static str],
|
||||
}
|
||||
|
||||
const PROVIDERS: &[Provider] = &[
|
||||
// Google
|
||||
Provider {
|
||||
host_suffixes: &["imap.gmail.com", ".gmail.com", ".googlemail.com"],
|
||||
issuer: "https://accounts.google.com",
|
||||
scopes: &["https://mail.google.com/"],
|
||||
},
|
||||
// Microsoft (Outlook / Office 365 / Hotmail / Live)
|
||||
Provider {
|
||||
host_suffixes: &[
|
||||
"outlook.office365.com",
|
||||
".outlook.com",
|
||||
".hotmail.com",
|
||||
".live.com",
|
||||
".office365.com",
|
||||
],
|
||||
issuer: "https://login.microsoftonline.com/common/v2.0",
|
||||
scopes: &[
|
||||
"https://outlook.office365.com/IMAP.AccessAsUser.All",
|
||||
"offline_access",
|
||||
],
|
||||
},
|
||||
// Yahoo / AOL / ATT / Verizon
|
||||
Provider {
|
||||
host_suffixes: &[
|
||||
"imap.mail.yahoo.com",
|
||||
".yahoo.com",
|
||||
".yahoodns.net",
|
||||
".aol.com",
|
||||
"imap.aol.com",
|
||||
],
|
||||
issuer: "https://login.yahoo.com",
|
||||
scopes: &["mail-w"],
|
||||
},
|
||||
// Yandex
|
||||
Provider {
|
||||
host_suffixes: &["imap.yandex.ru", "imap.yandex.com", ".yandex.ru"],
|
||||
issuer: "https://oauth.yandex.com",
|
||||
scopes: &["imap:all"],
|
||||
},
|
||||
// Mail.ru
|
||||
Provider {
|
||||
host_suffixes: &["imap.mail.ru", ".mail.ru", ".bk.ru", ".list.ru", ".inbox.ru"],
|
||||
issuer: "https://o2.mail.ru",
|
||||
scopes: &["imap"],
|
||||
},
|
||||
// Fastmail
|
||||
Provider {
|
||||
host_suffixes: &["imap.fastmail.com", ".fastmail.com"],
|
||||
issuer: "https://www.fastmail.com",
|
||||
scopes: &[
|
||||
"https://www.fastmail.com/dev/imap",
|
||||
"offline_access",
|
||||
],
|
||||
},
|
||||
// Comcast
|
||||
Provider {
|
||||
host_suffixes: &["imap.comcast.net", ".comcast.net"],
|
||||
issuer: "https://oauth.xfinity.com",
|
||||
scopes: &["https://email.comcast.net/"],
|
||||
},
|
||||
];
|
||||
|
||||
/// Try to find an OAuth2 provider that matches the given IMAP hostname.
|
||||
///
|
||||
/// Matching is case-insensitive and done by suffix: a hostname "imap.gmail.com"
|
||||
/// matches the suffix ".gmail.com".
|
||||
pub fn lookup_oauth2(hostname: &str) -> Option<OAuth2Config> {
|
||||
let host = hostname.to_ascii_lowercase();
|
||||
for provider in PROVIDERS {
|
||||
if provider
|
||||
.host_suffixes
|
||||
.iter()
|
||||
.any(|suffix| host.ends_with(&suffix.to_ascii_lowercase()))
|
||||
{
|
||||
return Some(OAuth2Config {
|
||||
issuer: provider.issuer.to_string(),
|
||||
scope: provider.scopes.iter().map(|s| s.to_string()).collect(),
|
||||
auth_url: String::new(),
|
||||
token_url: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_known_providers() {
|
||||
let cases = [
|
||||
("imap.gmail.com", Some("https://accounts.google.com")),
|
||||
("imap.gmail.com", Some("https://accounts.google.com")),
|
||||
("outlook.office365.com", Some("https://login.microsoftonline.com/common/v2.0")),
|
||||
("imap.mail.yahoo.com", Some("https://login.yahoo.com")),
|
||||
("imap.aol.com", Some("https://login.yahoo.com")),
|
||||
("imap.yandex.ru", Some("https://oauth.yandex.com")),
|
||||
("imap.mail.ru", Some("https://o2.mail.ru")),
|
||||
("imap.fastmail.com", Some("https://www.fastmail.com")),
|
||||
("imap.comcast.net", Some("https://oauth.xfinity.com")),
|
||||
];
|
||||
for (hostname, expected_issuer) in &cases {
|
||||
let result = lookup_oauth2(hostname);
|
||||
assert_eq!(
|
||||
result.map(|c| c.issuer),
|
||||
expected_issuer.map(|s| s.to_string()),
|
||||
"failed for hostname: {hostname}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_provider() {
|
||||
assert!(lookup_oauth2("mail.my-company.example").is_none());
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,7 @@ fn make_imap_server(host: &str, port: u16, socket_type: &str) -> IncomingServer
|
||||
port,
|
||||
socket_type: socket_type.to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +242,7 @@ fn convert_no_imap_only_pop3() {
|
||||
port: 995,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
};
|
||||
@@ -266,6 +268,7 @@ fn convert_picks_imap_over_pop3() {
|
||||
port: 995,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
},
|
||||
make_imap_server("imap.example.com", 993, "SSL"),
|
||||
],
|
||||
@@ -284,6 +287,7 @@ fn convert_imaps_protocol_variant() {
|
||||
port: 993,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
};
|
||||
@@ -300,9 +304,65 @@ fn convert_case_insensitive_protocol() {
|
||||
port: 143,
|
||||
socket_type: "STARTTLS".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: String::new(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
};
|
||||
let result = mail_config_to_server_config(&config).expect("should recognize 'IMAP'");
|
||||
assert_eq!(result.imap.host, "imap.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_gmail_oauth2() {
|
||||
let config = MailConfig {
|
||||
incoming: vec![IncomingServer {
|
||||
protocol: "imap".to_string(),
|
||||
hostname: "imap.gmail.com".to_string(),
|
||||
port: 993,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: "OAuth2".to_string(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
};
|
||||
let result = mail_config_to_server_config(&config).expect("should convert");
|
||||
let oauth2 = result.oauth2.expect("Gmail should have OAuth2");
|
||||
assert_eq!(oauth2.issuer, "https://accounts.google.com");
|
||||
assert!(oauth2.scope.contains(&"https://mail.google.com/".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_outlook_oauth2() {
|
||||
let config = MailConfig {
|
||||
incoming: vec![IncomingServer {
|
||||
protocol: "imap".to_string(),
|
||||
hostname: "outlook.office365.com".to_string(),
|
||||
port: 993,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: "OAuth2".to_string(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
};
|
||||
let result = mail_config_to_server_config(&config).expect("should convert");
|
||||
let oauth2 = result.oauth2.expect("Outlook should have OAuth2");
|
||||
assert!(oauth2.issuer.contains("microsoftonline"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_unknown_host_no_oauth2() {
|
||||
// OAuth2 auth flag on an unknown hostname → no OAuth2 returned
|
||||
let config = MailConfig {
|
||||
incoming: vec![IncomingServer {
|
||||
protocol: "imap".to_string(),
|
||||
hostname: "mail.random-isp.example".to_string(),
|
||||
port: 993,
|
||||
socket_type: "SSL".to_string(),
|
||||
username: "%EMAILADDRESS%".to_string(),
|
||||
authentication: "OAuth2".to_string(),
|
||||
}],
|
||||
outgoing: vec![],
|
||||
};
|
||||
let result = mail_config_to_server_config(&config).expect("should convert");
|
||||
assert!(result.oauth2.is_none(), "unknown hostname → no OAuth2 mapping");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user