/* * Copyright (c) 2023 Stalwart Labs Ltd. * * This file is part of the Stalwart JMAP Server. * * 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. * in the LICENSE file at the top-level directory of this distribution. * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * You can be released from the requirements of the AGPLv3 license by * purchasing a commercial license. Please contact licensing@stalw.art * for more details. */ use std::sync::Arc; use jmap_proto::{ error::request::RequestError, request::capability::Capability, response::serialize::serialize_hex, types::{acl::Acl, collection::Collection, id::Id}, }; use store::ahash::AHashSet; use utils::{listener::ServerInstance, map::vec_map::VecMap, UnwrapFailure}; use crate::{auth::AccessToken, JMAP}; #[derive(Debug, Clone, serde::Serialize)] pub struct Session { #[serde(rename(serialize = "capabilities"))] capabilities: VecMap, #[serde(rename(serialize = "accounts"))] accounts: VecMap, #[serde(rename(serialize = "primaryAccounts"))] primary_accounts: VecMap, #[serde(rename(serialize = "username"))] username: String, #[serde(rename(serialize = "apiUrl"))] api_url: String, #[serde(rename(serialize = "downloadUrl"))] download_url: String, #[serde(rename(serialize = "uploadUrl"))] upload_url: String, #[serde(rename(serialize = "eventSourceUrl"))] event_source_url: String, #[serde(rename(serialize = "state"))] #[serde(serialize_with = "serialize_hex")] state: u32, #[serde(skip)] base_url: String, } #[derive(Debug, Clone, serde::Serialize)] struct Account { #[serde(rename(serialize = "name"))] name: String, #[serde(rename(serialize = "isPersonal"))] is_personal: bool, #[serde(rename(serialize = "isReadOnly"))] is_read_only: bool, #[serde(rename(serialize = "accountCapabilities"))] account_capabilities: VecMap, } #[derive(Debug, Clone, serde::Serialize)] #[serde(untagged)] #[allow(dead_code)] enum Capabilities { Core(CoreCapabilities), Mail(MailCapabilities), Submission(SubmissionCapabilities), VacationResponse(VacationResponseCapabilities), WebSocket(WebSocketCapabilities), Sieve(SieveCapabilities), } #[derive(Debug, Clone, serde::Serialize)] struct CoreCapabilities { #[serde(rename(serialize = "maxSizeUpload"))] max_size_upload: usize, #[serde(rename(serialize = "maxConcurrentUpload"))] max_concurrent_upload: usize, #[serde(rename(serialize = "maxSizeRequest"))] max_size_request: usize, #[serde(rename(serialize = "maxConcurrentRequests"))] max_concurrent_requests: usize, #[serde(rename(serialize = "maxCallsInRequest"))] max_calls_in_request: usize, #[serde(rename(serialize = "maxObjectsInGet"))] max_objects_in_get: usize, #[serde(rename(serialize = "maxObjectsInSet"))] max_objects_in_set: usize, #[serde(rename(serialize = "collationAlgorithms"))] collation_algorithms: Vec, } #[derive(Debug, Clone, serde::Serialize)] struct WebSocketCapabilities { #[serde(rename(serialize = "url"))] url: String, #[serde(rename(serialize = "supportsPush"))] supports_push: bool, } #[derive(Debug, Clone, serde::Serialize)] struct SieveCapabilities { #[serde(rename(serialize = "implementation"))] implementation: &'static str, #[serde(rename(serialize = "maxSizeScriptName"))] max_script_name: usize, #[serde(rename(serialize = "maxSizeScript"))] max_script_size: usize, #[serde(rename(serialize = "maxNumberScripts"))] max_scripts: usize, #[serde(rename(serialize = "maxNumberRedirects"))] max_redirects: usize, #[serde(rename(serialize = "sieveExtensions"))] extensions: Vec, #[serde(rename(serialize = "notificationMethods"))] notification_methods: Option>, #[serde(rename(serialize = "externalLists"))] ext_lists: Option>, } #[derive(Debug, Clone, serde::Serialize)] struct MailCapabilities { #[serde(rename(serialize = "maxMailboxesPerEmail"))] max_mailboxes_per_email: Option, #[serde(rename(serialize = "maxMailboxDepth"))] max_mailbox_depth: usize, #[serde(rename(serialize = "maxSizeMailboxName"))] max_size_mailbox_name: usize, #[serde(rename(serialize = "maxSizeAttachmentsPerEmail"))] max_size_attachments_per_email: usize, #[serde(rename(serialize = "emailQuerySortOptions"))] email_query_sort_options: Vec, #[serde(rename(serialize = "mayCreateTopLevelMailbox"))] may_create_top_level_mailbox: bool, } #[derive(Debug, Clone, serde::Serialize)] struct SubmissionCapabilities { #[serde(rename(serialize = "maxDelayedSend"))] max_delayed_send: usize, #[serde(rename(serialize = "submissionExtensions"))] submission_extensions: Vec, } #[derive(Debug, Clone, serde::Serialize)] struct VacationResponseCapabilities {} #[derive(Default)] pub struct BaseCapabilities { capabilities: VecMap, } impl JMAP { pub async fn handle_session_resource( &self, instance: Arc, access_token: Arc, ) -> Result { let mut session = Session::new(&instance.data, &self.config.capabilities); session.set_state(access_token.state()); session.set_primary_account( access_token.primary_id().into(), access_token.name.clone(), access_token .description .clone() .unwrap_or_else(|| access_token.name.clone()), None, ); // Add secondary accounts for id in access_token.secondary_ids() { let is_personal = !access_token.is_member(*id); let is_readonly = is_personal && self .shared_documents(&access_token, *id, Collection::Mailbox, Acl::AddItems) .await .map_or(true, |ids| ids.is_empty()); session.add_account( (*id).into(), self.get_account_name(*id).await, is_personal, is_readonly, Some(&[Capability::Core, Capability::Mail, Capability::WebSocket]), ); } Ok(session) } pub async fn get_account_name(&self, account_id: u32) -> String { self.directory .principal_by_id(account_id) .await .unwrap_or_default() .map(|p| p.name) .unwrap_or_else(|| Id::from(account_id).to_string()) } } impl crate::Config { pub fn add_capabilites(&mut self, settings: &utils::config::Config) { self.capabilities.capabilities.append( Capability::Core, Capabilities::Core(CoreCapabilities::new(self)), ); self.capabilities.capabilities.append( Capability::Mail, Capabilities::Mail(MailCapabilities::new(self)), ); self.capabilities.capabilities.append( Capability::Submission, Capabilities::Submission(SubmissionCapabilities { max_delayed_send: 86400 * 30, submission_extensions: vec![ "FUTURERELEASE".to_string(), "SIZE".to_string(), "DSN".to_string(), "DELIVERYBY".to_string(), "MT-PRIORITY".to_string(), ], }), ); self.capabilities.capabilities.append( Capability::Sieve, Capabilities::Sieve(SieveCapabilities::new(self, settings)), ); } } impl Session { pub fn new(base_url: &str, base_capabilities: &BaseCapabilities) -> Session { let mut capabilities = base_capabilities.capabilities.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: base_url.to_string(), state: 0, } } pub fn set_primary_account( &mut self, account_id: Id, username: String, name: String, capabilities: Option<&[Capability]>, ) { self.username = username; if let Some(capabilities) = capabilities { for capability in capabilities { self.primary_accounts.append(*capability, account_id); } } else { for capability in self.capabilities.keys() { self.primary_accounts.append(*capability, account_id); } } self.accounts.set( account_id, Account::new(name, true, false).add_capabilities(capabilities, &self.capabilities), ); } pub fn add_account( &mut self, account_id: Id, name: String, is_personal: bool, is_read_only: bool, capabilities: Option<&[Capability]>, ) { self.accounts.set( account_id, Account::new(name, is_personal, is_read_only) .add_capabilities(capabilities, &self.capabilities), ); } 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 Account { pub fn new(name: String, is_personal: bool, is_read_only: bool) -> Account { Account { name, is_personal, is_read_only, account_capabilities: VecMap::new(), } } pub fn add_capabilities( mut self, capabilities: Option<&[Capability]>, core_capabilities: &VecMap, ) -> Account { if let Some(capabilities) = capabilities { for capability in capabilities { self.account_capabilities.append( *capability, core_capabilities.get(capability).unwrap().clone(), ); } } else { self.account_capabilities = core_capabilities.clone(); } self } } impl CoreCapabilities { pub fn new(config: &crate::Config) -> Self { CoreCapabilities { max_size_upload: config.upload_max_size, max_concurrent_upload: config.upload_max_concurrent, max_size_request: config.request_max_size, max_concurrent_requests: config.request_max_concurrent as usize, max_calls_in_request: config.request_max_calls, max_objects_in_get: config.get_max_objects, max_objects_in_set: config.set_max_objects, collation_algorithms: vec![ "i;ascii-numeric".to_string(), "i;ascii-casemap".to_string(), "i;unicode-casemap".to_string(), ], } } } impl WebSocketCapabilities { pub fn new(base_url: &str) -> Self { WebSocketCapabilities { url: format!("ws{}/jmap/ws", base_url.strip_prefix("http").unwrap()), supports_push: true, } } } impl SieveCapabilities { pub fn new(config: &crate::Config, settings: &utils::config::Config) -> Self { let mut notification_methods = Vec::new(); for (_, uri) in settings.values("jmap.sieve.notification-uris") { notification_methods.push(uri.to_string()); } if notification_methods.is_empty() { notification_methods.push("mailto".to_string()); } let mut capabilities: AHashSet = AHashSet::from_iter(sieve::compiler::grammar::Capability::all().iter().cloned()); for (_, capability) in settings.values("jmap.sieve.disabled-capabilities") { capabilities.remove(&sieve::compiler::grammar::Capability::parse(capability)); } let mut extensions = capabilities .into_iter() .map(|c| c.to_string()) .collect::>(); extensions.sort_unstable(); SieveCapabilities { max_script_name: config.sieve_max_script_name, max_script_size: settings .property("jmap.sieve.max-script-size") .failed("Invalid configuration file") .unwrap_or(1024 * 1024), max_scripts: config.sieve_max_scripts, max_redirects: settings .property("jmap.sieve.max-redirects") .failed("Invalid configuration file") .unwrap_or(1), extensions, notification_methods: if !notification_methods.is_empty() { notification_methods.into() } else { None }, ext_lists: None, implementation: concat!("Stalwart JMAP v", env!("CARGO_PKG_VERSION"),), } } } impl MailCapabilities { pub fn new(config: &crate::Config) -> Self { MailCapabilities { max_mailboxes_per_email: None, max_mailbox_depth: config.mailbox_max_depth, max_size_mailbox_name: config.mailbox_name_max_len, max_size_attachments_per_email: config.mail_attachments_max_size, email_query_sort_options: [ "receivedAt", "size", "from", "to", "subject", "sentAt", "hasKeyword", "allInThreadHaveKeyword", "someInThreadHaveKeyword", ] .iter() .map(|s| s.to_string()) .collect(), may_create_top_level_mailbox: true, } } }