Files
Stalwart/crates/common/src/config/jmap/capabilities.rs
2025-10-21 16:34:23 +02:00

268 lines
10 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::settings::JmapConfig;
use crate::config::groupware::GroupwareConfig;
use ahash::AHashSet;
use calcard::icalendar::ICalendarDuration;
use chrono::{DateTime, Utc};
use jmap_proto::{
object::email::EmailComparator,
request::capability::{
BlobCapabilities, CalendarCapabilities, Capabilities, Capability, ContactsCapabilities,
CoreCapabilities, EmptyCapabilities, FileNodeCapabilities, MailCapabilities,
PrincipalAvailabilityCapabilities, PrincipalCapabilities, SieveAccountCapabilities,
SieveSessionCapabilities, SubmissionCapabilities,
},
types::date::UTCDate,
};
use types::type_state::DataType;
use utils::{config::Config, map::vec_map::VecMap};
impl JmapConfig {
pub fn add_capabilities(&mut self, config: &mut Config, groupware_config: &GroupwareConfig) {
// Add core capabilities
self.capabilities.session.append(
Capability::Core,
Capabilities::Core(CoreCapabilities {
max_size_upload: self.upload_max_size,
max_concurrent_upload: self.upload_max_concurrent.unwrap_or(u32::MAX as u64)
as usize,
max_size_request: self.request_max_size,
max_concurrent_requests: self.request_max_concurrent.unwrap_or(u32::MAX as u64)
as usize,
max_calls_in_request: self.request_max_calls,
max_objects_in_get: self.get_max_objects,
max_objects_in_set: self.set_max_objects,
collation_algorithms: vec![
"i;ascii-numeric".to_string(),
"i;ascii-casemap".to_string(),
"i;unicode-casemap".to_string(),
],
}),
);
// Add email capabilities
self.capabilities.session.append(
Capability::Mail,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Mail,
Capabilities::Mail(MailCapabilities {
max_mailboxes_per_email: None,
max_mailbox_depth: self.mailbox_max_depth,
max_size_mailbox_name: self.mailbox_name_max_len,
max_size_attachments_per_email: self.mail_attachments_max_size,
email_query_sort_options: vec![
EmailComparator::ReceivedAt,
EmailComparator::Size,
EmailComparator::From,
EmailComparator::To,
EmailComparator::Subject,
EmailComparator::SentAt,
EmailComparator::HasKeyword(Default::default()),
EmailComparator::AllInThreadHaveKeyword(Default::default()),
EmailComparator::SomeInThreadHaveKeyword(Default::default()),
],
may_create_top_level_mailbox: true,
}),
);
// Add calendar capabilities
self.capabilities.session.append(
Capability::Calendars,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Calendars,
Capabilities::Calendar(CalendarCapabilities {
max_calendars_per_event: None,
min_date_time: UTCDate::from_timestamp(DateTime::<Utc>::MIN_UTC.timestamp()),
max_date_time: UTCDate::from_timestamp(DateTime::<Utc>::MAX_UTC.timestamp()),
max_expanded_query_duration: ICalendarDuration::from_seconds(86400 * 365)
.to_string(),
max_participants_per_event: groupware_config.max_ical_attendees_per_instance.into(),
may_create_calendar: true,
}),
);
self.capabilities.session.append(
Capability::CalendarsParse,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::CalendarsParse,
Capabilities::Empty(EmptyCapabilities::default()),
);
// Add contacts capabilities
self.capabilities.session.append(
Capability::Contacts,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Contacts,
Capabilities::Contacts(ContactsCapabilities {
max_address_books_per_card: None,
may_create_address_book: true,
}),
);
self.capabilities.session.append(
Capability::ContactsParse,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::ContactsParse,
Capabilities::Empty(EmptyCapabilities::default()),
);
// Add file node capabilities
self.capabilities.session.append(
Capability::FileNode,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::FileNode,
Capabilities::FileNode(FileNodeCapabilities {
max_file_node_depth: None,
max_size_file_node_name: 255,
file_node_query_sort_options: vec![],
may_create_top_level_file_node: true,
}),
);
// Add principal capabilities
self.capabilities.session.append(
Capability::Principals,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Principals,
Capabilities::Principals(PrincipalCapabilities {
current_user_principal_id: None,
}),
);
self.capabilities.session.append(
Capability::PrincipalsAvailability,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::PrincipalsAvailability,
Capabilities::PrincipalsAvailability(PrincipalAvailabilityCapabilities {
max_availability_duration: ICalendarDuration::from_seconds(86400 * 365).to_string(),
}),
);
// Add submission capabilities
self.capabilities.session.append(
Capability::Submission,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Submission,
Capabilities::Submission(SubmissionCapabilities {
max_delayed_send: 86400 * 30,
submission_extensions: VecMap::from_iter([
("FUTURERELEASE".to_string(), Vec::new()),
("SIZE".to_string(), Vec::new()),
("DSN".to_string(), Vec::new()),
("DELIVERYBY".to_string(), Vec::new()),
("MT-PRIORITY".to_string(), vec!["MIXER".to_string()]),
("REQUIRETLS".to_string(), vec![]),
]),
}),
);
// Add vacation response capabilities
self.capabilities.session.append(
Capability::VacationResponse,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::VacationResponse,
Capabilities::Empty(EmptyCapabilities::default()),
);
// Add Sieve capabilities
let mut notification_methods = Vec::new();
for (_, uri) in config.values("sieve.untrusted.notification-uris") {
notification_methods.push(uri.to_string());
}
if notification_methods.is_empty() {
notification_methods.push("mailto".to_string());
}
let mut capabilities: AHashSet<sieve::compiler::grammar::Capability> =
AHashSet::from_iter(sieve::compiler::grammar::Capability::all().iter().cloned());
for (_, capability) in config.values("sieve.untrusted.disabled-capabilities") {
capabilities.remove(&sieve::compiler::grammar::Capability::parse(capability));
}
let mut extensions = capabilities
.into_iter()
.map(|c| c.to_string())
.collect::<Vec<String>>();
extensions.sort_unstable();
self.capabilities.session.append(
Capability::Sieve,
Capabilities::SieveSession(SieveSessionCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Sieve,
Capabilities::SieveAccount(SieveAccountCapabilities {
max_script_name: self.sieve_max_script_name,
max_script_size: config
.property("sieve.untrusted.max-script-size")
.unwrap_or(1024 * 1024),
max_scripts: self.sieve_max_scripts,
max_redirects: config
.property("sieve.untrusted.max-redirects")
.unwrap_or(1),
extensions,
notification_methods: if !notification_methods.is_empty() {
notification_methods.into()
} else {
None
},
ext_lists: None,
}),
);
// Add Blob capabilities
self.capabilities.session.append(
Capability::Blob,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Blob,
Capabilities::Blob(BlobCapabilities {
max_size_blob_set: (self.request_max_size * 3 / 4) - 512,
max_data_sources: self.request_max_calls,
supported_type_names: vec![
DataType::Email,
DataType::Thread,
DataType::SieveScript,
],
supported_digest_algorithms: vec!["sha", "sha-256", "sha-512"],
}),
);
// Add Quota capabilities
self.capabilities.session.append(
Capability::Quota,
Capabilities::Empty(EmptyCapabilities::default()),
);
self.capabilities.account.insert(
Capability::Quota,
Capabilities::Empty(EmptyCapabilities::default()),
);
}
}