The urn:inbuxa:jmap capability on the signed-in principal's own account gains legacyProtocols: "enabled" or "disabled", the stricter of the server's switch and the account's tenant's (legacy-protocols spec, Interfaces). It is what the webmail needs to tell someone why their phone's mail app won't connect (LP-19), and it closes acceptance test 13. contract.md's C-1 gains the line. It is an optional field added, which C-3 says doesn't bump the contract version. tests/e2e/legacy_protocols.py reads it back from the session on a running server: enabled for the tenant's user while both switches are on, disabled once its tenant turns legacy protocols off while an account outside the tenant still reads enabled, disabled for everyone while the server switch is off, and enabled again at the end. All 67 checks pass.
551 lines
20 KiB
Rust
551 lines
20 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use std::fmt;
|
|
|
|
use crate::{
|
|
object::{email::EmailComparator, file_node::FileNodeComparator},
|
|
response::serialize::serialize_hex,
|
|
types::date::UTCDate,
|
|
};
|
|
use ahash::AHashMap;
|
|
use serde::{Deserialize, Deserializer};
|
|
use types::{id::Id, type_state::DataType};
|
|
use utils::map::vec_map::VecMap;
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct Session {
|
|
#[serde(rename(serialize = "capabilities"))]
|
|
pub capabilities: VecMap<Capability, Capabilities>,
|
|
#[serde(rename(serialize = "accounts"))]
|
|
pub accounts: VecMap<Id, Account>,
|
|
#[serde(rename(serialize = "primaryAccounts"))]
|
|
pub primary_accounts: VecMap<Capability, Id>,
|
|
#[serde(rename(serialize = "username"))]
|
|
pub username: String,
|
|
#[serde(rename(serialize = "apiUrl"))]
|
|
pub api_url: String,
|
|
#[serde(rename(serialize = "downloadUrl"))]
|
|
pub download_url: String,
|
|
#[serde(rename(serialize = "uploadUrl"))]
|
|
pub upload_url: String,
|
|
#[serde(rename(serialize = "eventSourceUrl"))]
|
|
pub event_source_url: String,
|
|
#[serde(rename(serialize = "state"))]
|
|
#[serde(serialize_with = "serialize_hex")]
|
|
pub state: u32,
|
|
#[serde(skip)]
|
|
pub base_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct Account {
|
|
#[serde(rename(serialize = "name"))]
|
|
pub name: String,
|
|
#[serde(rename(serialize = "isPersonal"))]
|
|
pub is_personal: bool,
|
|
#[serde(rename(serialize = "isReadOnly"))]
|
|
pub is_read_only: bool,
|
|
#[serde(rename(serialize = "accountCapabilities"))]
|
|
pub account_capabilities: VecMap<Capability, Capabilities>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum Capability {
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:core"))]
|
|
Core = 1 << 0,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:mail"))]
|
|
Mail = 1 << 1,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:submission"))]
|
|
Submission = 1 << 2,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:vacationresponse"))]
|
|
VacationResponse = 1 << 3,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:contacts"))]
|
|
Contacts = 1 << 4,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:contacts:parse"))]
|
|
ContactsParse = 1 << 5,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:calendars"))]
|
|
Calendars = 1 << 6,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:calendars:parse"))]
|
|
CalendarsParse = 1 << 7,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:websocket"))]
|
|
WebSocket = 1 << 8,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:sieve"))]
|
|
Sieve = 1 << 9,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:blob"))]
|
|
Blob = 1 << 10,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:quota"))]
|
|
Quota = 1 << 11,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:principals"))]
|
|
Principals = 1 << 12,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:principals:owner"))]
|
|
PrincipalsOwner = 1 << 13,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:principals:availability"))]
|
|
PrincipalsAvailability = 1 << 14,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:filenode"))]
|
|
FileNode = 1 << 15,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:mail:share"))]
|
|
MailShare = 1 << 16,
|
|
#[serde(rename(serialize = "urn:stalwart:jmap"))]
|
|
Stalwart = 1 << 17,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:webpush-vapid"))]
|
|
WebPushVapid = 1 << 18,
|
|
#[serde(rename(serialize = "urn:ietf:params:jmap:emailpush"))]
|
|
EmailPush = 1 << 19,
|
|
// inbuxa: the fork's own capability (contract C-1, multi-tenancy MT-22)
|
|
#[serde(rename(serialize = "urn:inbuxa:jmap"))]
|
|
Inbuxa = 1 << 20,
|
|
// inbuxa: Fastmail's Masked Email API (masked email)
|
|
#[serde(rename(serialize = "https://www.fastmail.com/dev/maskedemail"))]
|
|
FastmailMaskedEmail = 1 << 21,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
#[repr(transparent)]
|
|
pub struct CapabilityIds(pub u32);
|
|
|
|
impl CapabilityIds {
|
|
pub fn contains(&self, capability: Capability) -> bool {
|
|
self.0 & capability as u32 != 0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
#[serde(untagged)]
|
|
#[allow(dead_code)]
|
|
pub enum Capabilities {
|
|
Core(CoreCapabilities),
|
|
Mail(MailCapabilities),
|
|
Submission(SubmissionCapabilities),
|
|
WebSocket(WebSocketCapabilities),
|
|
SieveAccount(SieveAccountCapabilities),
|
|
SieveSession(SieveSessionCapabilities),
|
|
Blob(BlobCapabilities),
|
|
Contacts(ContactsCapabilities),
|
|
Principals(PrincipalCapabilities),
|
|
PrincipalsAvailability(PrincipalAvailabilityCapabilities),
|
|
Calendar(CalendarCapabilities),
|
|
FileNode(FileNodeCapabilities),
|
|
WebPush(WebPushCapabilities),
|
|
Inbuxa(InbuxaAccountCapabilities),
|
|
Empty(EmptyCapabilities),
|
|
}
|
|
|
|
/// inbuxa: `urn:inbuxa:jmap` on the signed-in principal's own account.
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct InbuxaAccountCapabilities {
|
|
/// The logo that applies to the principal (MT-22): a URL or a data URL.
|
|
#[serde(rename(serialize = "logo"))]
|
|
pub logo: Option<String>,
|
|
/// Whether legacy mail protocols are `enabled` or `disabled` for the
|
|
/// principal: the stricter of the server's switch and its tenant's
|
|
/// (legacy-protocols spec, Interfaces; LP-19).
|
|
#[serde(rename(serialize = "legacyProtocols"))]
|
|
pub legacy_protocols: &'static str,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct CoreCapabilities {
|
|
#[serde(rename(serialize = "maxSizeUpload"))]
|
|
pub max_size_upload: u64,
|
|
#[serde(rename(serialize = "maxConcurrentUpload"))]
|
|
pub max_concurrent_upload: u64,
|
|
#[serde(rename(serialize = "maxSizeRequest"))]
|
|
pub max_size_request: u64,
|
|
#[serde(rename(serialize = "maxConcurrentRequests"))]
|
|
pub max_concurrent_requests: u64,
|
|
#[serde(rename(serialize = "maxCallsInRequest"))]
|
|
pub max_calls_in_request: u64,
|
|
#[serde(rename(serialize = "maxObjectsInGet"))]
|
|
pub max_objects_in_get: u64,
|
|
#[serde(rename(serialize = "maxObjectsInSet"))]
|
|
pub max_objects_in_set: u64,
|
|
#[serde(rename(serialize = "collationAlgorithms"))]
|
|
pub collation_algorithms: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct WebSocketCapabilities {
|
|
#[serde(rename(serialize = "url"))]
|
|
pub url: String,
|
|
#[serde(rename(serialize = "supportsPush"))]
|
|
pub supports_push: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct SieveSessionCapabilities {
|
|
#[serde(rename(serialize = "implementation"))]
|
|
pub implementation: &'static str,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct SieveAccountCapabilities {
|
|
#[serde(rename(serialize = "maxSizeScriptName"))]
|
|
pub max_script_name: u64,
|
|
#[serde(rename(serialize = "maxSizeScript"))]
|
|
pub max_script_size: u64,
|
|
#[serde(rename(serialize = "maxNumberScripts"))]
|
|
pub max_scripts: u64,
|
|
#[serde(rename(serialize = "maxNumberRedirects"))]
|
|
pub max_redirects: u64,
|
|
#[serde(rename(serialize = "sieveExtensions"))]
|
|
pub extensions: Vec<String>,
|
|
#[serde(rename(serialize = "notificationMethods"))]
|
|
pub notification_methods: Option<Vec<String>>,
|
|
#[serde(rename(serialize = "externalLists"))]
|
|
pub ext_lists: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct MailCapabilities {
|
|
#[serde(rename(serialize = "maxMailboxesPerEmail"))]
|
|
pub max_mailboxes_per_email: Option<u64>,
|
|
#[serde(rename(serialize = "maxMailboxDepth"))]
|
|
pub max_mailbox_depth: u64,
|
|
#[serde(rename(serialize = "maxSizeMailboxName"))]
|
|
pub max_size_mailbox_name: u64,
|
|
#[serde(rename(serialize = "maxSizeAttachmentsPerEmail"))]
|
|
pub max_size_attachments_per_email: u64,
|
|
#[serde(rename(serialize = "emailQuerySortOptions"))]
|
|
pub email_query_sort_options: Vec<EmailComparator>,
|
|
#[serde(rename(serialize = "mayCreateTopLevelMailbox"))]
|
|
pub may_create_top_level_mailbox: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct SubmissionCapabilities {
|
|
#[serde(rename(serialize = "maxDelayedSend"))]
|
|
pub max_delayed_send: u64,
|
|
#[serde(rename(serialize = "submissionExtensions"))]
|
|
pub submission_extensions: VecMap<String, Vec<String>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct BlobCapabilities {
|
|
#[serde(rename(serialize = "maxSizeBlobSet"))]
|
|
pub max_size_blob_set: u64,
|
|
#[serde(rename(serialize = "maxDataSources"))]
|
|
pub max_data_sources: u64,
|
|
#[serde(rename(serialize = "supportedTypeNames"))]
|
|
pub supported_type_names: Vec<DataType>,
|
|
#[serde(rename(serialize = "supportedDigestAlgorithms"))]
|
|
pub supported_digest_algorithms: Vec<&'static str>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct CalendarCapabilities {
|
|
#[serde(rename(serialize = "maxCalendarsPerEvent"))]
|
|
pub max_calendars_per_event: Option<u64>,
|
|
#[serde(rename(serialize = "minDateTime"))]
|
|
pub min_date_time: UTCDate,
|
|
#[serde(rename(serialize = "maxDateTime"))]
|
|
pub max_date_time: UTCDate,
|
|
#[serde(rename(serialize = "maxExpandedQueryDuration"))]
|
|
pub max_expanded_query_duration: String,
|
|
#[serde(rename(serialize = "maxParticipantsPerEvent"))]
|
|
pub max_participants_per_event: Option<u64>,
|
|
#[serde(rename(serialize = "mayCreateCalendar"))]
|
|
pub may_create_calendar: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct ContactsCapabilities {
|
|
#[serde(rename(serialize = "maxAddressBooksPerCard"))]
|
|
pub max_address_books_per_card: Option<u64>,
|
|
#[serde(rename(serialize = "mayCreateAddressBook"))]
|
|
pub may_create_address_book: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct PrincipalAvailabilityCapabilities {
|
|
#[serde(rename(serialize = "maxAvailabilityDuration"))]
|
|
pub max_availability_duration: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct PrincipalCapabilities {
|
|
#[serde(rename(serialize = "currentUserPrincipalId"))]
|
|
pub current_user_principal_id: Option<Id>,
|
|
}
|
|
|
|
/*#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct PrincipalOwnerCapabilities {
|
|
#[serde(rename(serialize = "accountIdForPrincipal"))]
|
|
pub account_id_for_principal: Id,
|
|
|
|
#[serde(rename(serialize = "principalId"))]
|
|
pub principal_id: Id,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct PrincipalCalendarCapabilities {
|
|
#[serde(rename(serialize = "accountIdForPrincipal"))]
|
|
pub account_id_for_principal: Option<Id>,
|
|
#[serde(rename(serialize = "mayGetAvailability"))]
|
|
pub may_get_availability: bool,
|
|
#[serde(rename(serialize = "mayShareWith"))]
|
|
pub may_share_with: bool,
|
|
#[serde(rename(serialize = "calendarAddress"))]
|
|
pub calendar_address: String,
|
|
}*/
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct FileNodeCapabilities {
|
|
#[serde(rename(serialize = "maxFileNodeDepth"))]
|
|
pub max_file_node_depth: Option<u64>,
|
|
#[serde(rename(serialize = "maxSizeFileNodeName"))]
|
|
pub max_size_file_node_name: u64,
|
|
#[serde(rename(serialize = "forbiddenNameChars"))]
|
|
pub forbidden_name_chars: Option<String>,
|
|
#[serde(rename(serialize = "forbiddenNodeNames"))]
|
|
pub forbidden_node_names: Option<Vec<String>>,
|
|
#[serde(rename(serialize = "fileNodeQuerySortOptions"))]
|
|
pub file_node_query_sort_options: Vec<FileNodeComparator>,
|
|
#[serde(rename(serialize = "mayCreateTopLevelFileNode"))]
|
|
pub may_create_top_level_file_node: bool,
|
|
#[serde(rename(serialize = "caseInsensitiveNames"))]
|
|
pub case_insensitive_names: bool,
|
|
#[serde(rename(serialize = "webTrashUrl"))]
|
|
pub web_trash_url: Option<String>,
|
|
#[serde(rename(serialize = "webUrlTemplate"))]
|
|
pub web_url_template: Option<String>,
|
|
#[serde(rename(serialize = "webWriteUrlTemplate"))]
|
|
pub web_write_url_template: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct WebPushCapabilities {
|
|
#[serde(rename(serialize = "applicationServerKey"))]
|
|
pub application_server_key: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, serde::Serialize)]
|
|
pub struct EmptyCapabilities {}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct BaseCapabilities {
|
|
pub session: VecMap<Capability, Capabilities>,
|
|
pub account: AHashMap<Capability, Capabilities>,
|
|
}
|
|
|
|
impl Capability {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Capability::Core => "urn:ietf:params:jmap:core",
|
|
Capability::Mail => "urn:ietf:params:jmap:mail",
|
|
Capability::Submission => "urn:ietf:params:jmap:submission",
|
|
Capability::VacationResponse => "urn:ietf:params:jmap:vacationresponse",
|
|
Capability::Contacts => "urn:ietf:params:jmap:contacts",
|
|
Capability::ContactsParse => "urn:ietf:params:jmap:contacts:parse",
|
|
Capability::Calendars => "urn:ietf:params:jmap:calendars",
|
|
Capability::CalendarsParse => "urn:ietf:params:jmap:calendars:parse",
|
|
Capability::WebSocket => "urn:ietf:params:jmap:websocket",
|
|
Capability::Sieve => "urn:ietf:params:jmap:sieve",
|
|
Capability::Blob => "urn:ietf:params:jmap:blob",
|
|
Capability::Quota => "urn:ietf:params:jmap:quota",
|
|
Capability::Principals => "urn:ietf:params:jmap:principals",
|
|
Capability::PrincipalsOwner => "urn:ietf:params:jmap:principals:owner",
|
|
Capability::PrincipalsAvailability => "urn:ietf:params:jmap:principals:availability",
|
|
Capability::FileNode => "urn:ietf:params:jmap:filenode",
|
|
Capability::MailShare => "urn:ietf:params:jmap:mail:share",
|
|
Capability::Stalwart => "urn:stalwart:jmap",
|
|
Capability::WebPushVapid => "urn:ietf:params:jmap:webpush-vapid",
|
|
Capability::EmailPush => "urn:ietf:params:jmap:emailpush",
|
|
Capability::Inbuxa => "urn:inbuxa:jmap",
|
|
Capability::FastmailMaskedEmail => "https://www.fastmail.com/dev/maskedemail",
|
|
}
|
|
}
|
|
|
|
pub fn all_capabilities() -> &'static [Capability] {
|
|
&[
|
|
Capability::Core,
|
|
Capability::Mail,
|
|
Capability::Submission,
|
|
Capability::VacationResponse,
|
|
Capability::Contacts,
|
|
Capability::ContactsParse,
|
|
Capability::Calendars,
|
|
Capability::CalendarsParse,
|
|
Capability::WebSocket,
|
|
Capability::Sieve,
|
|
Capability::Blob,
|
|
Capability::Quota,
|
|
Capability::Principals,
|
|
Capability::PrincipalsAvailability,
|
|
Capability::FileNode,
|
|
Capability::MailShare,
|
|
Capability::Stalwart,
|
|
Capability::WebPushVapid,
|
|
Capability::EmailPush,
|
|
]
|
|
}
|
|
}
|
|
|
|
impl Session {
|
|
pub fn new(base_url: impl Into<String>, base_capabilities: &BaseCapabilities) -> Session {
|
|
let base_url = base_url.into();
|
|
let mut capabilities = base_capabilities.session.clone();
|
|
capabilities.append(
|
|
Capability::WebSocket,
|
|
Capabilities::WebSocket(WebSocketCapabilities::new(&base_url)),
|
|
);
|
|
|
|
Session {
|
|
capabilities,
|
|
accounts: VecMap::new(),
|
|
primary_accounts: VecMap::new(),
|
|
username: "".to_string(),
|
|
api_url: format!("{}/jmap/", base_url),
|
|
download_url: format!(
|
|
"{}/jmap/download/{{accountId}}/{{blobId}}/{{name}}?accept={{type}}",
|
|
base_url
|
|
),
|
|
upload_url: format!("{}/jmap/upload/{{accountId}}/", base_url),
|
|
event_source_url: format!(
|
|
"{}/jmap/eventsource/?types={{types}}&closeafter={{closeafter}}&ping={{ping}}",
|
|
base_url
|
|
),
|
|
base_url,
|
|
state: 0,
|
|
}
|
|
}
|
|
|
|
pub fn set_state(&mut self, state: u32) {
|
|
self.state = state;
|
|
}
|
|
|
|
pub fn api_url(&self) -> &str {
|
|
&self.api_url
|
|
}
|
|
|
|
pub fn base_url(&self) -> &str {
|
|
&self.base_url
|
|
}
|
|
}
|
|
|
|
impl Default for SieveSessionCapabilities {
|
|
fn default() -> Self {
|
|
Self {
|
|
implementation: concat!(types::brand!(), " ", types::brand_version_full!()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WebSocketCapabilities {
|
|
pub fn new(base_url: &str) -> Self {
|
|
WebSocketCapabilities {
|
|
url: format!(
|
|
"ws{}/jmap/ws",
|
|
base_url.strip_prefix("http").unwrap_or_default()
|
|
),
|
|
supports_push: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Capabilities {
|
|
pub fn to_account_capabilities(
|
|
&self,
|
|
current_user_principal_id: Option<Id>,
|
|
may_create: bool,
|
|
) -> Capabilities {
|
|
match self {
|
|
Capabilities::Contacts(contacts_capabilities) => {
|
|
Capabilities::Contacts(ContactsCapabilities {
|
|
may_create_address_book: may_create,
|
|
..contacts_capabilities.clone()
|
|
})
|
|
}
|
|
Capabilities::Principals(_) => Capabilities::Principals(PrincipalCapabilities {
|
|
current_user_principal_id,
|
|
}),
|
|
Capabilities::Calendar(calendar_capabilities) => {
|
|
Capabilities::Calendar(CalendarCapabilities {
|
|
may_create_calendar: may_create,
|
|
..calendar_capabilities.clone()
|
|
})
|
|
}
|
|
Capabilities::FileNode(file_node_capabilities) => {
|
|
Capabilities::FileNode(FileNodeCapabilities {
|
|
may_create_top_level_file_node: may_create,
|
|
..file_node_capabilities.clone()
|
|
})
|
|
}
|
|
_ => self.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Capability {
|
|
pub fn parse(s: &str) -> Option<Self> {
|
|
hashify::tiny_map!(s.as_bytes(),
|
|
"urn:ietf:params:jmap:core" => Capability::Core,
|
|
"urn:ietf:params:jmap:mail" => Capability::Mail,
|
|
"urn:ietf:params:jmap:submission" => Capability::Submission,
|
|
"urn:ietf:params:jmap:vacationresponse" => Capability::VacationResponse,
|
|
"urn:ietf:params:jmap:contacts" => Capability::Contacts,
|
|
"urn:ietf:params:jmap:calendars" => Capability::Calendars,
|
|
"urn:ietf:params:jmap:websocket" => Capability::WebSocket,
|
|
"urn:ietf:params:jmap:sieve" => Capability::Sieve,
|
|
"urn:ietf:params:jmap:blob" => Capability::Blob,
|
|
"urn:ietf:params:jmap:quota" => Capability::Quota,
|
|
"urn:ietf:params:jmap:principals" => Capability::Principals,
|
|
"urn:ietf:params:jmap:principals:owner" => Capability::PrincipalsOwner,
|
|
"urn:ietf:params:jmap:filenode" => Capability::FileNode,
|
|
"urn:ietf:params:jmap:principals:availability" => Capability::PrincipalsAvailability,
|
|
"urn:ietf:params:jmap:contacts:parse" => Capability::ContactsParse,
|
|
"urn:ietf:params:jmap:calendars:parse" => Capability::CalendarsParse,
|
|
"urn:ietf:params:jmap:mail:share" => Capability::MailShare,
|
|
"urn:stalwart:jmap" => Capability::Stalwart,
|
|
"urn:ietf:params:jmap:webpush-vapid" => Capability::WebPushVapid,
|
|
"urn:ietf:params:jmap:emailpush" => Capability::EmailPush,
|
|
"urn:inbuxa:jmap" => Capability::Inbuxa,
|
|
"https://www.fastmail.com/dev/maskedemail" => Capability::FastmailMaskedEmail,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for CapabilityIds {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
struct CapabilityIdsVisitor;
|
|
|
|
impl<'de> serde::de::Visitor<'de> for CapabilityIdsVisitor {
|
|
type Value = CapabilityIds;
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
formatter.write_str("an array of capability strings")
|
|
}
|
|
|
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
|
where
|
|
A: serde::de::SeqAccess<'de>,
|
|
{
|
|
let mut capability_flags = 0u32;
|
|
|
|
while let Some(capability_str) = seq.next_element::<std::borrow::Cow<str>>()? {
|
|
let capability =
|
|
Capability::parse(capability_str.as_ref()).ok_or_else(|| {
|
|
serde::de::Error::custom(format!(
|
|
"Unknown capability: {capability_str:?}"
|
|
))
|
|
})?;
|
|
|
|
capability_flags |= capability as u32;
|
|
}
|
|
|
|
Ok(CapabilityIds(capability_flags))
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_seq(CapabilityIdsVisitor)
|
|
}
|
|
}
|