Files
Stalwart/crates/common/src/config/jmap/settings.rs
2025-11-04 17:45:37 +01:00

426 lines
17 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::config::groupware::GroupwareConfig;
use ahash::{AHashMap, AHashSet};
use jmap_proto::request::capability::BaseCapabilities;
use nlp::language::Language;
use std::{str::FromStr, time::Duration};
use store::{search::SearchField, write::SearchIndex};
use types::{collection::Collection, special_use::SpecialUse};
use utils::{
config::{Config, Rate, cron::SimpleCron, utils::ParseValue},
map::bitmap::Bitmap,
};
#[derive(Default, Clone)]
pub struct JmapConfig {
pub default_language: Language,
pub query_max_results: usize,
pub snippet_max_results: usize,
pub changes_max_results: Option<usize>,
pub changes_max_history: Option<usize>,
pub share_notification_max_history: Option<Duration>,
pub request_max_size: usize,
pub request_max_calls: usize,
pub request_max_concurrent: Option<u64>,
pub get_max_objects: usize,
pub set_max_objects: usize,
pub upload_max_size: usize,
pub upload_max_concurrent: Option<u64>,
pub upload_tmp_quota_size: usize,
pub upload_tmp_quota_amount: usize,
pub upload_tmp_ttl: u64,
pub mailbox_max_depth: usize,
pub mailbox_name_max_len: usize,
pub mail_attachments_max_size: usize,
pub mail_parse_max_items: usize,
pub mail_max_size: usize,
pub mail_autoexpunge_after: Option<u64>,
pub email_submission_autoexpunge_after: Option<u64>,
pub contact_parse_max_items: usize,
pub calendar_parse_max_items: usize,
pub sieve_max_script_name: usize,
pub max_objects: [u32; Collection::MAX],
pub rate_authenticated: Option<Rate>,
pub rate_anonymous: Option<Rate>,
pub event_source_throttle: Duration,
pub push_attempt_interval: Duration,
pub push_attempts_max: u32,
pub push_retry_interval: Duration,
pub push_timeout: Duration,
pub push_verify_timeout: Duration,
pub push_throttle: Duration,
pub web_socket_throttle: Duration,
pub web_socket_timeout: Duration,
pub web_socket_heartbeat: Duration,
pub fallback_admin: Option<(String, String)>,
pub master_user: Option<(String, String)>,
pub default_folders: Vec<DefaultFolder>,
pub shared_folder: String,
pub http_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>,
pub http_use_forwarded: bool,
pub encrypt: bool,
pub encrypt_append: bool,
pub index_batch_size: usize,
pub index_fields: AHashMap<SearchIndex, AHashSet<SearchField>>,
pub capabilities: BaseCapabilities,
pub account_purge_frequency: SimpleCron,
}
#[derive(Clone, Debug)]
pub struct DefaultFolder {
pub name: String,
pub aliases: Vec<String>,
pub special_use: SpecialUse,
pub subscribe: bool,
pub create: bool,
}
impl JmapConfig {
pub fn parse(config: &mut Config, groupware_config: &GroupwareConfig) -> Self {
// Parse HTTP headers
let mut http_headers = config
.values("http.headers")
.map(|(_, v)| {
if let Some((k, v)) = v.split_once(':') {
Ok((
hyper::header::HeaderName::from_str(k.trim()).map_err(|err| {
format!("Invalid header found in property \"http.headers\": {}", err)
})?,
hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| {
format!("Invalid header found in property \"http.headers\": {}", err)
})?,
))
} else {
Err(format!(
"Invalid header found in property \"http.headers\": {}",
v
))
}
})
.collect::<Result<Vec<_>, String>>()
.map_err(|e| config.new_parse_error("http.headers", e))
.unwrap_or_default();
// Parse default folders
let mut default_folders = Vec::new();
let mut shared_folder = "Shared Folders".to_string();
for key in config.sub_keys("email.folders", ".name") {
match SpecialUse::parse_value(&key) {
Ok(SpecialUse::Shared) => {
if let Some(value) = config.value(("email.folders", key.as_str(), "name")) {
shared_folder = value.to_string();
}
}
Ok(special_use) => {
let subscribe = config
.property_or_default(("email.folders", key.as_str(), "subscribe"), "true")
.unwrap_or(true);
let create = config
.property_or_default(("email.folders", key.as_str(), "create"), "true")
.unwrap_or(true)
| [SpecialUse::Inbox, SpecialUse::Trash, SpecialUse::Junk]
.contains(&special_use);
if let Some(name) = config
.value(("email.folders", key.as_str(), "name"))
.map(|name| name.trim())
.filter(|name| !name.is_empty())
{
default_folders.push(DefaultFolder {
name: name.to_string(),
aliases: config
.value(("email.folders", key.as_str(), "aliases"))
.unwrap_or_default()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
special_use,
subscribe,
create,
});
}
}
Err(err) => {
config.new_parse_error(key, err);
}
}
}
for (special_use, name) in [
(SpecialUse::Inbox, "Inbox"),
(SpecialUse::Trash, "Deleted Items"),
(SpecialUse::Junk, "Junk Mail"),
(SpecialUse::Drafts, "Drafts"),
(SpecialUse::Sent, "Sent Items"),
] {
if !default_folders.iter().any(|f| f.special_use == special_use) {
default_folders.push(DefaultFolder {
name: name.to_string(),
aliases: Vec::new(),
special_use,
subscribe: true,
create: true,
});
}
}
// Add permissive CORS headers
if config
.property::<bool>("http.permissive-cors")
.unwrap_or(false)
{
http_headers.push((
hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN,
hyper::header::HeaderValue::from_static("*"),
));
http_headers.push((
hyper::header::ACCESS_CONTROL_ALLOW_HEADERS,
hyper::header::HeaderValue::from_static(
"Authorization, Content-Type, Accept, X-Requested-With",
),
));
http_headers.push((
hyper::header::ACCESS_CONTROL_ALLOW_METHODS,
hyper::header::HeaderValue::from_static(
"POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
),
));
}
// Add HTTP Strict Transport Security
if config.property::<bool>("http.hsts").unwrap_or(false) {
http_headers.push((
hyper::header::STRICT_TRANSPORT_SECURITY,
hyper::header::HeaderValue::from_static("max-age=31536000; includeSubDomains"),
));
}
let mut jmap = JmapConfig {
default_language: Language::from_iso_639(
config
.value("storage.full-text.default-language")
.unwrap_or("en"),
)
.unwrap_or(Language::English),
query_max_results: config
.property("jmap.protocol.query.max-results")
.unwrap_or(5000),
changes_max_results: config
.property_or_default::<Option<usize>>("jmap.protocol.changes.max-results", "5000")
.unwrap_or_default(),
changes_max_history: config
.property_or_default::<Option<usize>>("changes.max-history", "10000")
.unwrap_or_default(),
share_notification_max_history: config
.property_or_default::<Option<Duration>>("sharing.max-history", "30d")
.unwrap_or_default(),
snippet_max_results: config
.property("jmap.protocol.search-snippet.max-results")
.unwrap_or(100),
request_max_size: config
.property("jmap.protocol.request.max-size")
.unwrap_or(10000000),
request_max_calls: config
.property("jmap.protocol.request.max-calls")
.unwrap_or(16),
request_max_concurrent: config
.property_or_default::<Option<u64>>("jmap.protocol.request.max-concurrent", "4")
.unwrap_or(Some(4)),
get_max_objects: config
.property("jmap.protocol.get.max-objects")
.unwrap_or(500),
set_max_objects: config
.property("jmap.protocol.set.max-objects")
.unwrap_or(500),
upload_max_size: config
.property("jmap.protocol.upload.max-size")
.unwrap_or(50000000),
upload_max_concurrent: config
.property_or_default::<Option<u64>>("jmap.protocol.upload.max-concurrent", "4")
.unwrap_or(Some(4)),
upload_tmp_quota_size: config
.property("jmap.protocol.upload.quota.size")
.unwrap_or(50000000),
upload_tmp_quota_amount: config
.property("jmap.protocol.upload.quota.files")
.unwrap_or(1000),
upload_tmp_ttl: config
.property_or_default::<Duration>("jmap.protocol.upload.ttl", "1h")
.unwrap_or_else(|| Duration::from_secs(3600))
.as_secs(),
mailbox_max_depth: config.property("jmap.mailbox.max-depth").unwrap_or(10),
mailbox_name_max_len: config
.property("jmap.mailbox.max-name-length")
.unwrap_or(255),
mail_attachments_max_size: config
.property("jmap.email.max-attachment-size")
.unwrap_or(50000000),
mail_max_size: config.property("jmap.email.max-size").unwrap_or(75000000),
mail_parse_max_items: config.property("jmap.email.parse.max-items").unwrap_or(10),
mail_autoexpunge_after: config
.property_or_default::<Option<Duration>>("email.auto-expunge", "30d")
.map(|d| d.map(|d| d.as_secs()))
.unwrap_or_default(),
email_submission_autoexpunge_after: config
.property_or_default::<Option<Duration>>("email-submission.auto-expunge", "3d")
.map(|d| d.map(|d| d.as_secs()))
.unwrap_or_default(),
sieve_max_script_name: config
.property("sieve.untrusted.limits.name-length")
.unwrap_or(512),
max_objects: [u32::MAX; Collection::MAX],
capabilities: BaseCapabilities::default(),
rate_authenticated: config
.property_or_default::<Option<Rate>>("http.rate-limit.account", "1000/1m")
.unwrap_or_default(),
rate_anonymous: config
.property_or_default::<Option<Rate>>("http.rate-limit.anonymous", "100/1m")
.unwrap_or_default(),
event_source_throttle: config
.property_or_default("jmap.event-source.throttle", "1s")
.unwrap_or_else(|| Duration::from_secs(1)),
web_socket_throttle: config
.property_or_default("jmap.web-socket.throttle", "1s")
.unwrap_or_else(|| Duration::from_secs(1)),
web_socket_timeout: config
.property_or_default("jmap.web-socket.timeout", "10m")
.unwrap_or_else(|| Duration::from_secs(10 * 60)),
web_socket_heartbeat: config
.property_or_default("jmap.web-socket.heartbeat", "1m")
.unwrap_or_else(|| Duration::from_secs(60)),
encrypt: config
.property_or_default("email.encryption.enable", "true")
.unwrap_or(true),
encrypt_append: config
.property_or_default("email.encryption.append", "false")
.unwrap_or(false),
http_use_forwarded: config.property("http.use-x-forwarded").unwrap_or(false),
http_headers,
push_attempt_interval: config
.property_or_default("jmap.push.attempts.interval", "1m")
.unwrap_or_else(|| Duration::from_secs(60)),
push_attempts_max: config
.property_or_default("jmap.push.attempts.max", "3")
.unwrap_or(3),
push_retry_interval: config
.property_or_default("jmap.push.retry.interval", "1s")
.unwrap_or_else(|| Duration::from_secs(1)),
push_timeout: config
.property_or_default("jmap.push.timeout.request", "10s")
.unwrap_or_else(|| Duration::from_secs(10)),
push_verify_timeout: config
.property_or_default("jmap.push.timeout.verify", "1m")
.unwrap_or_else(|| Duration::from_secs(60)),
push_throttle: config
.property_or_default("jmap.push.throttle", "1s")
.unwrap_or_else(|| Duration::from_secs(1)),
account_purge_frequency: config
.property_or_default::<SimpleCron>("account.purge.frequency", "0 0 *")
.unwrap_or_else(|| SimpleCron::parse_value("0 0 *").unwrap()),
fallback_admin: config
.value("authentication.fallback-admin.user")
.and_then(|u| {
config
.value("authentication.fallback-admin.secret")
.map(|p| (u.to_string(), p.to_string()))
}),
master_user: config.value("authentication.master.user").and_then(|u| {
config
.value("authentication.master.secret")
.map(|p| (u.to_string(), p.to_string()))
}),
contact_parse_max_items: config
.property("jmap.contact.parse.max-items")
.unwrap_or(10),
calendar_parse_max_items: config
.property("jmap.calendar.parse.max-items")
.unwrap_or(10),
index_batch_size: config.property("jmap.index.batch-size").unwrap_or(100),
index_fields: AHashMap::new(),
default_folders,
shared_folder,
};
// Parse index fields
for index in [
SearchIndex::Email,
SearchIndex::Contacts,
SearchIndex::Calendar,
] {
let mut fields = AHashSet::new();
let todo = "implement";
/*for field_str in config.values(&format!(
"jmap.index.{}.fields",
index.as_config_case()
)) {
match SearchField::try_from(field_str.1.as_str()) {
Ok(field) => {
fields.insert(field);
}
Err(_) => {
config.new_parse_error(
&format!(
"jmap.index.{}.fields",
index.as_config_case()
),
format!("Invalid search field: {}", field_str.1),
);
}
}
}*/
jmap.index_fields.insert(index, fields);
}
for collection in Bitmap::<Collection>::all() {
let key = format!("object-quota.{}", collection.as_config_case());
jmap.max_objects[collection as usize] =
if let Some(value) = config.property::<u32>(&key) {
value
} else {
match collection {
Collection::Mailbox => 250,
Collection::SieveScript => 100,
Collection::Identity => 20,
Collection::EmailSubmission => 500,
Collection::PushSubscription => 15,
Collection::Calendar => 250,
Collection::AddressBook => 250,
Collection::Principal
| Collection::None
| Collection::CalendarEventNotification
| Collection::CalendarEvent
| Collection::ContactCard
| Collection::FileNode
| Collection::Email
| Collection::Thread => u32::MAX,
}
};
}
// Add capabilities
jmap.add_capabilities(config, groupware_config);
jmap
}
}