/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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, #[serde(rename(serialize = "accounts"))] pub accounts: VecMap, #[serde(rename(serialize = "primaryAccounts"))] pub primary_accounts: VecMap, #[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, } #[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), 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, /// 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, } #[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, #[serde(rename(serialize = "notificationMethods"))] pub notification_methods: Option>, #[serde(rename(serialize = "externalLists"))] pub ext_lists: Option>, } #[derive(Debug, Clone, serde::Serialize)] pub struct MailCapabilities { #[serde(rename(serialize = "maxMailboxesPerEmail"))] pub max_mailboxes_per_email: Option, #[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, #[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>, } #[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, #[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, #[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, #[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, #[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, } /*#[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, #[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, #[serde(rename(serialize = "maxSizeFileNodeName"))] pub max_size_file_node_name: u64, #[serde(rename(serialize = "forbiddenNameChars"))] pub forbidden_name_chars: Option, #[serde(rename(serialize = "forbiddenNodeNames"))] pub forbidden_node_names: Option>, #[serde(rename(serialize = "fileNodeQuerySortOptions"))] pub file_node_query_sort_options: Vec, #[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, #[serde(rename(serialize = "webUrlTemplate"))] pub web_url_template: Option, #[serde(rename(serialize = "webWriteUrlTemplate"))] pub web_write_url_template: Option, } #[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, pub account: AHashMap, } 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, 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, 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 { 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(deserializer: D) -> Result 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(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut capability_flags = 0u32; while let Some(capability_str) = seq.next_element::>()? { 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) } }