Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
284 lines
10 KiB
Rust
284 lines
10 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use calcard::vcard::VCardVersion;
|
|
use registry::schema::{
|
|
enums::VCardVersion as RegistryVCardVersion,
|
|
structs::{
|
|
AddressBook, Calendar, CalendarAlarm, CalendarScheduling, DataRetention, FileStorage,
|
|
Sharing, SystemSettings, WebDav,
|
|
},
|
|
};
|
|
use std::str::FromStr;
|
|
use store::registry::bootstrap::Bootstrap;
|
|
use utils::template::Template;
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct GroupwareConfig {
|
|
// DAV settings
|
|
pub max_request_size: usize,
|
|
pub dead_property_size: Option<usize>,
|
|
pub live_property_size: usize,
|
|
pub max_lock_timeout: u64,
|
|
pub max_locks_per_user: usize,
|
|
pub max_results: usize,
|
|
pub assisted_discovery: bool,
|
|
|
|
// Calendar settings
|
|
pub max_ical_size: usize,
|
|
pub max_ical_instances: usize,
|
|
pub max_ical_attendees_per_instance: usize,
|
|
pub default_calendar_name: Option<String>,
|
|
pub default_calendar_display_name: Option<String>,
|
|
pub alarms_enabled: bool,
|
|
pub alarms_minimum_interval: i64,
|
|
pub alarms_allow_external_recipients: bool,
|
|
pub alarms_from_name: String,
|
|
pub alarms_from_email: Option<String>,
|
|
pub alarms_template: Template<CalendarTemplateVariable>,
|
|
pub itip_enabled: bool,
|
|
pub itip_auto_add: bool,
|
|
pub itip_inbound_max_ical_size: usize,
|
|
pub itip_outbound_max_recipients: usize,
|
|
pub itip_http_rsvp_url: Option<String>,
|
|
pub itip_http_rsvp_expiration: u64,
|
|
pub itip_inbox_auto_expunge: Option<u64>,
|
|
pub itip_template: Template<CalendarTemplateVariable>,
|
|
|
|
// Addressbook settings
|
|
pub max_vcard_size: usize,
|
|
pub vcard_version: VCardVersion,
|
|
pub default_addressbook_name: Option<String>,
|
|
pub default_addressbook_display_name: Option<String>,
|
|
|
|
// File storage settings
|
|
pub max_file_size: usize,
|
|
|
|
// Sharing settings
|
|
pub max_shares_per_item: usize,
|
|
pub allow_directory_query: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
|
|
pub enum CalendarTemplateVariable {
|
|
#[default]
|
|
PageTitle,
|
|
Lang,
|
|
Dir,
|
|
Header,
|
|
Footer,
|
|
EventTitle,
|
|
EventDescription,
|
|
EventDetails,
|
|
Actions,
|
|
ActionUrl,
|
|
ActionName,
|
|
AttendeesTitle,
|
|
Attendees,
|
|
Key,
|
|
Color,
|
|
Changed,
|
|
Value,
|
|
Link,
|
|
LogoCid,
|
|
OldValue,
|
|
Rsvp,
|
|
}
|
|
|
|
impl GroupwareConfig {
|
|
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
|
let calendar = bp.setting_infallible::<Calendar>().await;
|
|
let alarm = bp.setting_infallible::<CalendarAlarm>().await;
|
|
let sched = bp.setting_infallible::<CalendarScheduling>().await;
|
|
let book = bp.setting_infallible::<AddressBook>().await;
|
|
let dav = bp.setting_infallible::<WebDav>().await;
|
|
let file = bp.setting_infallible::<FileStorage>().await;
|
|
let share = bp.setting_infallible::<Sharing>().await;
|
|
let dr = bp.setting_infallible::<DataRetention>().await;
|
|
let system = bp.setting_infallible::<SystemSettings>().await;
|
|
|
|
GroupwareConfig {
|
|
max_request_size: dav.request_max_size as usize,
|
|
dead_property_size: dav.dead_property_max_size.map(|v| v as usize),
|
|
live_property_size: dav.live_property_max_size as usize,
|
|
assisted_discovery: dav.enable_assisted_discovery,
|
|
max_lock_timeout: dav.max_lock_timeout.into_inner().as_secs(),
|
|
max_locks_per_user: dav.max_locks as usize,
|
|
max_results: dav.max_results as usize,
|
|
default_calendar_name: calendar.default_href_name,
|
|
default_calendar_display_name: calendar.default_display_name,
|
|
default_addressbook_name: book.default_href_name,
|
|
default_addressbook_display_name: book.default_display_name,
|
|
max_ical_size: calendar.max_i_calendar_size as usize,
|
|
max_ical_instances: calendar.max_recurrence_expansions as usize,
|
|
max_ical_attendees_per_instance: calendar.max_attendees as usize,
|
|
max_vcard_size: book.max_v_card_size as usize,
|
|
vcard_version: match book.v_card_version {
|
|
RegistryVCardVersion::V3 => VCardVersion::V3_0,
|
|
RegistryVCardVersion::V4 => VCardVersion::V4_0,
|
|
},
|
|
max_file_size: file.max_size as usize,
|
|
alarms_enabled: alarm.enable,
|
|
alarms_minimum_interval: alarm.min_trigger_interval.into_inner().as_secs() as i64,
|
|
alarms_allow_external_recipients: alarm.allow_external_rcpts,
|
|
alarms_from_name: alarm.from_name,
|
|
alarms_from_email: alarm.from_email,
|
|
alarms_template: Template::parse(include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../resources/html-templates/calendar-alarm.html.min"
|
|
)))
|
|
.expect("Failed to parse calendar template"),
|
|
itip_enabled: sched.enable,
|
|
itip_auto_add: sched.auto_add_invitations,
|
|
itip_inbound_max_ical_size: sched.itip_max_size as usize,
|
|
itip_outbound_max_recipients: sched.max_recipients as usize,
|
|
itip_inbox_auto_expunge: dr
|
|
.expunge_scheduling_inbox_after
|
|
.map(|d| d.into_inner().as_secs()),
|
|
itip_http_rsvp_url: if sched.http_rsvp_enable {
|
|
if let Some(url) = sched
|
|
.http_rsvp_url
|
|
.as_deref()
|
|
.map(|v| v.trim().trim_end_matches('/'))
|
|
.filter(|v| !v.is_empty())
|
|
{
|
|
Some(url.to_string())
|
|
} else {
|
|
Some(format!("https://{}/calendar/rsvp", system.default_hostname))
|
|
}
|
|
} else {
|
|
None
|
|
},
|
|
max_shares_per_item: share.max_shares as usize,
|
|
allow_directory_query: share.allow_directory_queries,
|
|
itip_http_rsvp_expiration: sched.http_rsvp_link_expiry.into_inner().as_secs(),
|
|
itip_template: Template::parse(include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../resources/html-templates/calendar-invite.html.min"
|
|
)))
|
|
.expect("Failed to parse calendar template"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for CalendarTemplateVariable {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"page_title" => Ok(CalendarTemplateVariable::PageTitle),
|
|
"lang" => Ok(CalendarTemplateVariable::Lang),
|
|
"dir" => Ok(CalendarTemplateVariable::Dir),
|
|
"header" => Ok(CalendarTemplateVariable::Header),
|
|
"footer" => Ok(CalendarTemplateVariable::Footer),
|
|
"event_title" => Ok(CalendarTemplateVariable::EventTitle),
|
|
"event_description" => Ok(CalendarTemplateVariable::EventDescription),
|
|
"event_details" => Ok(CalendarTemplateVariable::EventDetails),
|
|
"action_url" => Ok(CalendarTemplateVariable::ActionUrl),
|
|
"action_name" => Ok(CalendarTemplateVariable::ActionName),
|
|
"attendees" => Ok(CalendarTemplateVariable::Attendees),
|
|
"attendees_title" => Ok(CalendarTemplateVariable::AttendeesTitle),
|
|
"key" => Ok(CalendarTemplateVariable::Key),
|
|
"value" => Ok(CalendarTemplateVariable::Value),
|
|
"link" => Ok(CalendarTemplateVariable::Link),
|
|
"logo_cid" => Ok(CalendarTemplateVariable::LogoCid),
|
|
"actions" => Ok(CalendarTemplateVariable::Actions),
|
|
"changed" => Ok(CalendarTemplateVariable::Changed),
|
|
"old_value" => Ok(CalendarTemplateVariable::OldValue),
|
|
"rsvp" => Ok(CalendarTemplateVariable::Rsvp),
|
|
"color" => Ok(CalendarTemplateVariable::Color),
|
|
_ => Err(format!("Unknown calendar template variable: {}", s)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::CalendarTemplateVariable;
|
|
use utils::template::Template;
|
|
|
|
const TEMPLATES: [(&str, &str, &str); 2] = [
|
|
(
|
|
"calendar-invite.html",
|
|
include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../resources/html-templates/calendar-invite.html"
|
|
)),
|
|
include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../resources/html-templates/calendar-invite.html.min"
|
|
)),
|
|
),
|
|
(
|
|
"calendar-alarm.html",
|
|
include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../resources/html-templates/calendar-alarm.html"
|
|
)),
|
|
include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../resources/html-templates/calendar-alarm.html.min"
|
|
)),
|
|
),
|
|
];
|
|
|
|
// Every `{{...}}` token in a template, in order of appearance
|
|
fn tokens(contents: &str) -> Vec<&str> {
|
|
let mut tokens = Vec::new();
|
|
let mut rest = contents;
|
|
|
|
while let Some((_, after)) = rest.split_once("{{") {
|
|
match after.split_once("}}") {
|
|
Some((token, tail)) => {
|
|
tokens.push(token.trim());
|
|
rest = tail;
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
|
|
tokens
|
|
}
|
|
|
|
#[test]
|
|
fn shipped_calendar_templates_parse() {
|
|
for (name, source, minified) in TEMPLATES {
|
|
Template::<CalendarTemplateVariable>::parse(source)
|
|
.unwrap_or_else(|err| panic!("{name} failed to parse: {err}"));
|
|
Template::<CalendarTemplateVariable>::parse(minified)
|
|
.unwrap_or_else(|err| panic!("{name}.min failed to parse: {err}"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn minified_calendar_templates_are_in_sync() {
|
|
for (name, source, minified) in TEMPLATES {
|
|
let source = tokens(source);
|
|
assert!(source.len() > 10, "{name} yielded no tokens to compare");
|
|
assert_eq!(
|
|
source,
|
|
tokens(minified),
|
|
"{name}.min is stale, re-run resources/scripts/minify_html.sh"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn calendar_template_tokens_are_single_line() {
|
|
// A newline inside `{{...}}` makes the parser reject the block
|
|
for (name, source, minified) in TEMPLATES {
|
|
for (suffix, contents) in [("", source), (".min", minified)] {
|
|
for token in tokens(contents) {
|
|
assert!(
|
|
!token.contains('\n') && !token.contains('\r'),
|
|
"{name}{suffix} has a multi-line token: {token:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|