Files
inbuxa-server/crates/jmap-proto/src/request/capability.rs
T
jcoffey-dev 8e9cedbe97
ci / fork-checks (pull_request) Successful in 43s
ci / build (pull_request) Successful in 7m40s
Give IMAP, POP3 and ManageSieve a switch each
The legacy-protocols switch was all or nothing. An operator can now stop
POP3 and keep IMAP: each of IMAP, POP3 and ManageSieve has its own
switch, server-wide on inbuxa:ProtocolPolicy and per tenant on
inbuxa:TenantProtocolPolicy (properties imap, pop3, manageSieve).

legacyProtocols stays as the kill-all: setting it sets all three, and it
reads "disabled" exactly when all three are off. A policy stored before
this has only legacyProtocols and reads as all three at that value, so
existing servers and tenants carry over unchanged. In one /set, a
protocol named beside legacyProtocols overrides it.

SMTP submission keeps no switch of its own: sign-in over it is refused
only when all three are off, as the single switch did (LP-6), so
turning one protocol off never stops a mail app sending. For a tenant,
the server's switches and the tenant's count together.

Server-wide, a change closes the listeners of whatever is now off and
puts back the saved listeners of whatever is on again, both in one
change if asked; listeners of a protocol still off stay saved. Sign-in,
autoconfig, autodiscover, PACC (now prepared once per combination) and
the suggested DNS records all follow each protocol separately. A tenant
may turn a protocol on only while the server has it on (LP-9), and the
refusal names which. The JMAP session adds legacyAllowed, the protocols
still allowed for the account; legacyProtocols there keeps its meaning
for older webmail builds. Events name the switches ("pop3 disabled"),
and audit before/after reads every switch even from an older policy.

Tested: unit tests for the switches, the old-policy reading, the
server/tenant combination, the tenant refusal and listener refusal; and
tests/e2e/legacy_protocols.py against a running server, all 100 checks,
including new ones: POP3 alone off closes only its port and refuses
only its sign-in while IMAP and sending go on; only POP3 stops being
advertised; one change closes IMAP and reopens POP3; a tenant turns
POP3 off for itself, and can't turn IMAP on while the server has it off.
2026-09-27 23:12:32 -07:00

583 lines
22 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:inbuxa:jmap:registry"))]
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),
// inbuxa: AL-7
InbuxaDelegated(InbuxaDelegatedCapabilities),
Empty(EmptyCapabilities),
}
/// inbuxa: `urn:inbuxa:jmap` on a locked account delegated to the signed-in
/// principal (audit-hold-lock spec, AL-7), so a client can tell it from an
/// ordinary share without guessing from `isReadOnly`.
#[derive(Debug, Clone, serde::Serialize)]
pub struct InbuxaDelegatedCapabilities {
pub delegation: DelegationInfo,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct DelegationInfo {
/// Always true: only locked accounts are delegated.
pub locked: bool,
/// `read`, `organize` or `full`.
pub access: &'static str,
#[serde(rename(serialize = "sendAs"))]
pub send_as: bool,
/// When the delegation ends, if it does (UTC).
pub until: Option<String>,
}
/// 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,
/// The legacy protocols still allowed for the principal, each the
/// stricter of the two switches: `imap`, `pop3`, `manageSieve`,
/// `submission` (legacy-protocols spec, one switch per protocol).
#[serde(rename(serialize = "legacyAllowed"))]
pub legacy_allowed: Vec<&'static str>,
/// Whether the principal may use "Explain this" now: it holds
/// `sysAiExplain`, is server-level, and a model resolves (ai-explain
/// spec, EX-1 to EX-4).
#[serde(rename(serialize = "aiExplain"))]
pub ai_explain: bool,
}
#[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:inbuxa:jmap:registry",
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:inbuxa:jmap:registry" => 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)
}
}