434 lines
17 KiB
Rust
434 lines
17 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use std::{str::FromStr, time::Duration};
|
|
|
|
use jmap_proto::request::capability::BaseCapabilities;
|
|
use nlp::language::Language;
|
|
use utils::config::{Config, Rate, cron::SimpleCron, utils::ParseValue};
|
|
|
|
#[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 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 sieve_max_script_name: usize,
|
|
pub sieve_max_scripts: usize,
|
|
|
|
pub rate_authenticated: Option<Rate>,
|
|
pub rate_anonymous: Option<Rate>,
|
|
|
|
pub event_source_throttle: Duration,
|
|
pub push_max_total: usize,
|
|
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 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,
|
|
}
|
|
|
|
#[derive(
|
|
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug,
|
|
)]
|
|
#[rkyv(derive(Debug))]
|
|
pub enum SpecialUse {
|
|
Inbox,
|
|
Trash,
|
|
Junk,
|
|
Drafts,
|
|
Archive,
|
|
Sent,
|
|
Shared,
|
|
Important,
|
|
None,
|
|
}
|
|
|
|
impl JmapConfig {
|
|
pub fn parse(config: &mut Config) -> 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")
|
|
.map(|v| v.to_string())
|
|
.collect::<Vec<_>>()
|
|
{
|
|
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; preload",
|
|
),
|
|
));
|
|
}
|
|
|
|
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(),
|
|
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(),
|
|
sieve_max_script_name: config
|
|
.property("sieve.untrusted.limits.name-length")
|
|
.unwrap_or(512),
|
|
sieve_max_scripts: config
|
|
.property("sieve.untrusted.limits.max-scripts")
|
|
.unwrap_or(256),
|
|
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)),
|
|
push_max_total: config
|
|
.property_or_default("jmap.push.max-total", "100")
|
|
.unwrap_or(100),
|
|
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()))
|
|
}),
|
|
default_folders,
|
|
shared_folder,
|
|
};
|
|
|
|
// Add capabilities
|
|
jmap.add_capabilities(config);
|
|
jmap
|
|
}
|
|
}
|
|
|
|
impl ParseValue for SpecialUse {
|
|
fn parse_value(value: &str) -> Result<Self, String> {
|
|
hashify::tiny_map_ignore_case!(value.as_bytes(),
|
|
b"inbox" => SpecialUse::Inbox,
|
|
b"trash" => SpecialUse::Trash,
|
|
b"junk" => SpecialUse::Junk,
|
|
b"drafts" => SpecialUse::Drafts,
|
|
b"archive" => SpecialUse::Archive,
|
|
b"sent" => SpecialUse::Sent,
|
|
b"shared" => SpecialUse::Shared,
|
|
b"important" => SpecialUse::Important,
|
|
|
|
)
|
|
.ok_or_else(|| format!("Unknown folder role {:?}", value))
|
|
}
|
|
}
|
|
|
|
impl SpecialUse {
|
|
pub fn as_str(&self) -> Option<&'static str> {
|
|
match self {
|
|
SpecialUse::Inbox => Some("inbox"),
|
|
SpecialUse::Trash => Some("trash"),
|
|
SpecialUse::Junk => Some("junk"),
|
|
SpecialUse::Drafts => Some("drafts"),
|
|
SpecialUse::Archive => Some("archive"),
|
|
SpecialUse::Sent => Some("sent"),
|
|
SpecialUse::Shared => Some("shared"),
|
|
SpecialUse::Important => Some("important"),
|
|
SpecialUse::None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ArchivedSpecialUse {
|
|
pub fn as_str(&self) -> Option<&'static str> {
|
|
match self {
|
|
ArchivedSpecialUse::Inbox => Some("inbox"),
|
|
ArchivedSpecialUse::Trash => Some("trash"),
|
|
ArchivedSpecialUse::Junk => Some("junk"),
|
|
ArchivedSpecialUse::Drafts => Some("drafts"),
|
|
ArchivedSpecialUse::Archive => Some("archive"),
|
|
ArchivedSpecialUse::Sent => Some("sent"),
|
|
ArchivedSpecialUse::Shared => Some("shared"),
|
|
ArchivedSpecialUse::Important => Some("important"),
|
|
ArchivedSpecialUse::None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&ArchivedSpecialUse> for SpecialUse {
|
|
fn from(value: &ArchivedSpecialUse) -> Self {
|
|
match value {
|
|
ArchivedSpecialUse::Inbox => SpecialUse::Inbox,
|
|
ArchivedSpecialUse::Trash => SpecialUse::Trash,
|
|
ArchivedSpecialUse::Junk => SpecialUse::Junk,
|
|
ArchivedSpecialUse::Drafts => SpecialUse::Drafts,
|
|
ArchivedSpecialUse::Archive => SpecialUse::Archive,
|
|
ArchivedSpecialUse::Sent => SpecialUse::Sent,
|
|
ArchivedSpecialUse::Shared => SpecialUse::Shared,
|
|
ArchivedSpecialUse::Important => SpecialUse::Important,
|
|
ArchivedSpecialUse::None => SpecialUse::None,
|
|
}
|
|
}
|
|
}
|