Files
inbuxa-server/crates/common/src/network/autoconfig/pacc.rs
T
jcoffey-dev 7dae9b29fd Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
2026-09-18 10:21:56 -07:00

359 lines
11 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use serde::{Deserialize, Serialize};
/// Top-level configuration document.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Configuration {
/// Supported protocols and their server endpoints.
pub protocols: Protocols,
/// Authentication mechanisms the provider supports.
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<Authentication>,
/// Informational metadata about the provider.
pub info: Info,
}
/// The `protocols` object listing available protocol endpoints.
///
/// HTTP-based protocols (JMAP, CalDAV, CardDAV, WebDAV) use [`HttpServer`].
/// Text-based protocols (IMAP, POP3, SMTP, ManageSieve) use [`TextServer`].
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Protocols {
#[serde(skip_serializing_if = "Option::is_none")]
pub jmap: Option<HttpServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub imap: Option<TextServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pop3: Option<TextServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub smtp: Option<TextServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub caldav: Option<HttpServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub carddav: Option<HttpServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webdav: Option<HttpServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub managesieve: Option<TextServer>,
}
/// An HTTP-based protocol endpoint (JMAP, CalDAV, CardDAV, WebDAV).
///
/// The `url` MUST use the `https` scheme and the default port 443.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HttpServer {
/// HTTPS URL of the protocol endpoint.
pub url: String,
}
/// A text-based protocol endpoint (IMAP, POP3, SMTP, ManageSieve).
///
/// Connections use TLS on the protocol's default port
/// (993 IMAP, 995 POP3, 465 SMTP).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextServer {
/// Hostname of the server.
pub host: String,
}
/// Authentication mechanisms supported by the provider.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Authentication {
/// OAuth Profile for Open Public Clients configuration.
#[serde(rename = "oauth-public", skip_serializing_if = "Option::is_none")]
pub oauth_public: Option<OAuthPublic>,
/// Whether the provider supports username/password authentication.
pub password: bool,
}
/// OAuth Profile for Open Public Clients parameters.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OAuthPublic {
/// The authorization server's issuer identifier (RFC 8414).
/// Must be an `https` URL with no query or fragment components.
pub issuer: String,
}
/// Informational metadata presented to users and developers.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Info {
/// Provider identity information (required).
pub provider: Provider,
/// Help links for users and developers.
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<Help>,
}
/// Provider identity.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Provider {
/// Display name of the provider (≤ 60 characters, SHOULD ≤ 30).
pub name: String,
/// Short name (≤ 20 characters, SHOULD ≤ 12).
#[serde(rename = "shortName", skip_serializing_if = "Option::is_none")]
pub short_name: Option<String>,
/// Logo image variants.
#[serde(skip_serializing_if = "Option::is_none")]
pub logo: Option<Vec<Logo>>,
}
/// A single logo variant.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Logo {
/// URL where the logo can be retrieved.
pub url: String,
/// Media type of the logo image (e.g. `image/svg+xml`, `image/png`).
#[serde(rename = "content-type")]
pub content_type: String,
/// Image width in pixels. Omitted for SVG.
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
/// Image height in pixels. Omitted for SVG.
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
}
/// Help links for users and developers.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Help {
/// URL with user-facing documentation.
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
/// URL with developer-facing documentation.
#[serde(skip_serializing_if = "Option::is_none")]
pub developer: Option<String>,
/// Contact URIs (e.g. `mailto:` URLs). NOT for end-user display.
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
/// The full example from Section 4.1 of the draft.
const EXAMPLE_JSON: &str = r#"{
"protocols": {
"jmap": {
"url": "https://jmap.example.com/session"
},
"imap": {
"host": "imap.example.com"
},
"pop3": {
"host": "pop3.example.com"
},
"smtp": {
"host": "smtp.example.com"
},
"caldav": {
"url": "https://sync.example.com/calendar/"
},
"carddav": {
"url": "https://sync.example.com/contacts/"
}
},
"authentication": {
"oauth-public": {
"issuer": "https://auth.example.com/"
},
"password": true
},
"info": {
"provider": {
"name": "Example Provider Name",
"shortName": "Example",
"logo": [
{
"url": "https://www.example.net/logo.svg",
"content-type": "image/svg+xml"
}
]
},
"help": {
"documentation": "https://help.example.net/howto/set-up-your-mail-app.html",
"developer": "https://developer.example.net/client-apps/",
"contact": ["mailto:[email protected]"]
}
}
}"#;
#[test]
fn deserialize_full_example() {
let config: Configuration =
serde_json::from_str(EXAMPLE_JSON).expect("failed to deserialize");
// Protocols
assert_eq!(
config.protocols.jmap.as_ref().unwrap().url,
"https://jmap.example.com/session"
);
assert_eq!(
config.protocols.imap.as_ref().unwrap().host,
"imap.example.com"
);
assert_eq!(
config.protocols.smtp.as_ref().unwrap().host,
"smtp.example.com"
);
assert_eq!(
config.protocols.pop3.as_ref().unwrap().host,
"pop3.example.com"
);
assert_eq!(
config.protocols.caldav.as_ref().unwrap().url,
"https://sync.example.com/calendar/"
);
assert_eq!(
config.protocols.carddav.as_ref().unwrap().url,
"https://sync.example.com/contacts/"
);
assert!(config.protocols.webdav.is_none());
assert!(config.protocols.managesieve.is_none());
// Authentication
let auth = config.authentication.as_ref().unwrap();
assert!(auth.password);
assert_eq!(
auth.oauth_public.as_ref().unwrap().issuer,
"https://auth.example.com/"
);
// Info
assert_eq!(config.info.provider.name, "Example Provider Name");
assert_eq!(config.info.provider.short_name.as_deref(), Some("Example"));
let logos = config.info.provider.logo.as_ref().unwrap();
assert_eq!(logos.len(), 1);
assert_eq!(logos[0].content_type, "image/svg+xml");
assert!(logos[0].width.is_none());
let help = config.info.help.as_ref().unwrap();
assert_eq!(
help.documentation.as_deref(),
Some("https://help.example.net/howto/set-up-your-mail-app.html")
);
assert_eq!(
help.contact.as_ref().unwrap(),
&["mailto:[email protected]"]
);
}
#[test]
fn roundtrip() {
let config: Configuration =
serde_json::from_str(EXAMPLE_JSON).expect("failed to deserialize");
let serialized = serde_json::to_string_pretty(&config).expect("failed to serialize");
let roundtripped: Configuration =
serde_json::from_str(&serialized).expect("failed to re-deserialize");
assert_eq!(config, roundtripped);
}
#[test]
fn minimal_config() {
let json = r#"{
"protocols": {},
"info": {
"provider": {
"name": "Minimal"
}
}
}"#;
let config: Configuration = serde_json::from_str(json).expect("failed to deserialize");
assert_eq!(config.info.provider.name, "Minimal");
assert!(config.authentication.is_none());
assert!(config.protocols.jmap.is_none());
}
#[test]
fn ignores_unknown_properties() {
let json = r#"{
"protocols": {
"imap": { "host": "imap.example.com" },
"future-protocol": { "endpoint": "wss://example.com" }
},
"info": {
"provider": { "name": "Test" }
},
"futureField": 42
}"#;
let config: Configuration = serde_json::from_str(json).expect("should ignore unknowns");
assert_eq!(
config.protocols.imap.as_ref().unwrap().host,
"imap.example.com"
);
}
#[test]
fn logo_with_dimensions() {
let json = r#"{
"protocols": {},
"info": {
"provider": {
"name": "Test",
"logo": [
{
"url": "https://example.com/logo.svg",
"content-type": "image/svg+xml"
},
{
"url": "https://example.com/logo-128.png",
"content-type": "image/png",
"width": 128,
"height": 128
},
{
"url": "https://example.com/logo-512.png",
"content-type": "image/png",
"width": 512,
"height": 512
}
]
}
}
}"#;
let config: Configuration = serde_json::from_str(json).unwrap();
let logos = config.info.provider.logo.as_ref().unwrap();
assert_eq!(logos.len(), 3);
assert!(logos[0].width.is_none());
assert_eq!(logos[1].width, Some(128));
assert_eq!(logos[2].height, Some(512));
}
#[test]
fn password_only_auth() {
let json = r#"{
"protocols": { "imap": { "host": "mail.example.com" } },
"authentication": { "password": true },
"info": { "provider": { "name": "PW Only" } }
}"#;
let config: Configuration = serde_json::from_str(json).unwrap();
let auth = config.authentication.unwrap();
assert!(auth.password);
assert!(auth.oauth_public.is_none());
}
}