Import upstream v0.16.22, stripped
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.
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use calcard::icalendar::{
|
||||
ArchivedICalendarParameterName, ArchivedICalendarProperty, ICalendarProperty,
|
||||
};
|
||||
use common::{
|
||||
DEFAULT_LOGO_BASE64, Server,
|
||||
auth::{AccountInfo, BuildAccessToken},
|
||||
config::groupware::CalendarTemplateVariable,
|
||||
ipc::{CalendarAlert, PushNotification},
|
||||
network::{ServerInstance, stream::NullIo},
|
||||
};
|
||||
use groupware::{
|
||||
calendar::{ArchivedCalendarEvent, CalendarEvent},
|
||||
scheduling::{
|
||||
ItipTime, ItipValue,
|
||||
format::{DateStyle, TextFormatter, hyperlink},
|
||||
},
|
||||
strip_mailto_scheme,
|
||||
};
|
||||
use mail_builder::{
|
||||
MessageBuilder,
|
||||
headers::{HeaderType, content_type::ContentType},
|
||||
mime::{BodyPart, MimePart},
|
||||
};
|
||||
use mail_parser::decoders::html::html_to_text;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::Permission,
|
||||
structs::{TaskCalendarAlarmEmail, TaskCalendarAlarmNotification},
|
||||
},
|
||||
types::EnumImpl,
|
||||
};
|
||||
use smtp::core::{Session, SessionData};
|
||||
use smtp_proto::{MailFrom, RcptTo};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, now},
|
||||
};
|
||||
use trc::{AddContext, TaskManagerEvent};
|
||||
use types::collection::Collection;
|
||||
use utils::{sanitize_email, template::Variables};
|
||||
|
||||
use crate::task_manager::TaskResult;
|
||||
|
||||
pub(crate) trait SendAlarmTask: Sync + Send {
|
||||
fn send_display_alarm(
|
||||
&self,
|
||||
task: &TaskCalendarAlarmNotification,
|
||||
) -> impl Future<Output = TaskResult> + Send;
|
||||
|
||||
fn send_email_alarm(
|
||||
&self,
|
||||
task: &TaskCalendarAlarmEmail,
|
||||
server_instance: Arc<ServerInstance>,
|
||||
) -> impl Future<Output = TaskResult> + Send;
|
||||
}
|
||||
|
||||
impl SendAlarmTask for Server {
|
||||
async fn send_display_alarm(&self, task: &TaskCalendarAlarmNotification) -> TaskResult {
|
||||
match send_display_alarm(self, task).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
let result = TaskResult::temporary(err.to_string());
|
||||
trc::error!(
|
||||
err.account_id(task.account_id.document_id())
|
||||
.document_id(task.document_id.document_id())
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to process e-mail alarm")
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_email_alarm(
|
||||
&self,
|
||||
task: &TaskCalendarAlarmEmail,
|
||||
server_instance: Arc<ServerInstance>,
|
||||
) -> TaskResult {
|
||||
match send_email_alarm(self, task, server_instance).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
let result = TaskResult::temporary(err.to_string());
|
||||
trc::error!(
|
||||
err.account_id(task.account_id.document_id())
|
||||
.document_id(task.document_id.document_id())
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to process e-mail alarm")
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_email_alarm(
|
||||
server: &Server,
|
||||
task: &TaskCalendarAlarmEmail,
|
||||
server_instance: Arc<ServerInstance>,
|
||||
) -> trc::Result<TaskResult> {
|
||||
// Obtain access token
|
||||
let account_id = task.account_id.document_id();
|
||||
let document_id = task.document_id.document_id();
|
||||
let access_token = server
|
||||
.access_token(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.build();
|
||||
|
||||
if !access_token.has_permission(Permission::CalendarAlarmsSend) {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::AlarmSkipped),
|
||||
Reason = "Account does not have permission to send calendar alarms",
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
return Ok(TaskResult::Success(vec![]));
|
||||
}
|
||||
let account_info = server
|
||||
.account_info(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if account_info.name().is_empty() {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::AlarmFailed),
|
||||
Reason = "Account does not have any email addresses",
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
return Ok(TaskResult::Success(vec![]));
|
||||
}
|
||||
|
||||
// Fetch event
|
||||
let Some(event_) = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
trc::event!(
|
||||
TaskManager(TaskManagerEvent::MetadataNotFound),
|
||||
Details = "Calendar Event metadata not found",
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
|
||||
return Ok(TaskResult::Success(vec![]));
|
||||
};
|
||||
|
||||
// Unarchive event
|
||||
let event = event_
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Build message body
|
||||
let account_main_email = account_info.name();
|
||||
let account_main_domain = account_main_email.rsplit('@').next().unwrap_or("localhost");
|
||||
let logo_cid = format!("logo.{}@{account_main_domain}", now());
|
||||
let Some(tpl) = build_template(server, &account_info, task, event, &logo_cid).await? else {
|
||||
return Ok(TaskResult::Success(vec![]));
|
||||
};
|
||||
let txt_body = html_to_text(&tpl.body);
|
||||
|
||||
// Obtain logo image
|
||||
let logo = match server.logo_resource(account_main_domain).await {
|
||||
Ok(logo) => logo,
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.caused_by(trc::location!())
|
||||
.details("Failed to fetch logo image")
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
let logo = if let Some(logo) = &logo {
|
||||
MimePart::new(
|
||||
ContentType::new(logo.content_type.as_ref()),
|
||||
BodyPart::Binary(logo.contents.as_slice().into()),
|
||||
)
|
||||
} else {
|
||||
MimePart::new(
|
||||
ContentType::new("image/png"),
|
||||
BodyPart::Binary(DEFAULT_LOGO_BASE64.as_bytes().into()),
|
||||
)
|
||||
.transfer_encoding("base64")
|
||||
}
|
||||
.inline()
|
||||
.cid(&logo_cid);
|
||||
|
||||
// Build message
|
||||
let mail_from = if let Some(from_email) = &server.core.groupware.alarms_from_email {
|
||||
from_email.to_string()
|
||||
} else {
|
||||
format!("calendar-notification@{account_main_domain}")
|
||||
};
|
||||
let message = MessageBuilder::new()
|
||||
.from((
|
||||
server.core.groupware.alarms_from_name.as_str(),
|
||||
mail_from.as_str(),
|
||||
))
|
||||
.header("To", HeaderType::Text(tpl.to.as_str().into()))
|
||||
.header("Auto-Submitted", HeaderType::Text("auto-generated".into()))
|
||||
.header("Reply-To", HeaderType::Text(account_main_email.into()))
|
||||
.message_id(server.core.network.message_id())
|
||||
.subject(tpl.subject)
|
||||
.body(MimePart::new(
|
||||
ContentType::new("multipart/related"),
|
||||
BodyPart::Multipart(vec![
|
||||
MimePart::new(
|
||||
ContentType::new("multipart/alternative"),
|
||||
BodyPart::Multipart(vec![
|
||||
MimePart::new(
|
||||
ContentType::new("text/plain"),
|
||||
BodyPart::Text(txt_body.into()),
|
||||
),
|
||||
MimePart::new(
|
||||
ContentType::new("text/html"),
|
||||
BodyPart::Text(tpl.body.into()),
|
||||
),
|
||||
]),
|
||||
),
|
||||
logo,
|
||||
]),
|
||||
))
|
||||
.write_to_vec()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Send message
|
||||
let server_ = server.clone();
|
||||
let mail_from = account_main_email.to_string();
|
||||
let to = tpl.to;
|
||||
let result = tokio::spawn(async move {
|
||||
let mut session = Session::<NullIo>::local(
|
||||
server_,
|
||||
server_instance,
|
||||
SessionData::local(account_info, None, vec![], vec![], 0),
|
||||
);
|
||||
|
||||
// MAIL FROM
|
||||
let _ = session
|
||||
.handle_mail_from(MailFrom {
|
||||
address: mail_from.into(),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
if let Some(error) = session.has_failed() {
|
||||
return Err(format!("Server rejected MAIL-FROM: {}", error.trim()));
|
||||
}
|
||||
|
||||
// RCPT TO
|
||||
session.params.rcpt_errors_wait = Duration::from_secs(0);
|
||||
let _ = session
|
||||
.handle_rcpt_to(RcptTo {
|
||||
address: to.into(),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
if let Some(error) = session.has_failed() {
|
||||
return Err(format!("Server rejected RCPT-TO: {}", error.trim()));
|
||||
}
|
||||
|
||||
// DATA
|
||||
session.data.message = message;
|
||||
let response = session.queue_message().await;
|
||||
if let smtp::core::State::Accepted(queue_id) = session.state {
|
||||
Ok(queue_id)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Server rejected DATA: {}",
|
||||
std::str::from_utf8(&response).unwrap().trim()
|
||||
))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(queue_id)) => {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::AlarmSent),
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
QueueId = queue_id,
|
||||
);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::AlarmFailed),
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
Reason = err,
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
trc::event!(
|
||||
Server(trc::ServerEvent::ThreadError),
|
||||
Details = "Join Error",
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
CausedBy = trc::location!(),
|
||||
);
|
||||
return Ok(TaskResult::temporary("Thread join error"));
|
||||
}
|
||||
}
|
||||
|
||||
build_next_alarm(server, account_id, document_id, event)
|
||||
}
|
||||
|
||||
async fn send_display_alarm(
|
||||
server: &Server,
|
||||
task: &TaskCalendarAlarmNotification,
|
||||
) -> trc::Result<TaskResult> {
|
||||
// Fetch event
|
||||
let account_id = task.account_id.document_id();
|
||||
let document_id = task.document_id.document_id();
|
||||
let Some(event_) = server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
trc::event!(
|
||||
TaskManager(TaskManagerEvent::MetadataNotFound),
|
||||
Details = "Calendar Event metadata not found",
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
|
||||
return Ok(TaskResult::Success(vec![]));
|
||||
};
|
||||
|
||||
// Unarchive event
|
||||
let event = event_
|
||||
.unarchive::<CalendarEvent>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let recurrence_id = task.recurrence_id;
|
||||
|
||||
let ical = &event.data.event;
|
||||
server
|
||||
.broadcast_push_notification(PushNotification::CalendarAlert(CalendarAlert {
|
||||
account_id,
|
||||
event_id: document_id,
|
||||
recurrence_id,
|
||||
uid: ical.uids().next().unwrap_or_default().to_string(),
|
||||
alert_id: ical
|
||||
.components
|
||||
.get(task.alarm_id as usize)
|
||||
.and_then(|c| c.property(&ICalendarProperty::Jsid))
|
||||
.and_then(|v| v.values.first())
|
||||
.and_then(|v| v.as_text())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"k{}",
|
||||
ical.components
|
||||
.get(task.event_id as usize)
|
||||
.and_then(|c| c
|
||||
.component_ids
|
||||
.iter()
|
||||
.position(|id| id.to_native() == task.alarm_id as u32))
|
||||
.unwrap_or_default()
|
||||
+ 1
|
||||
)
|
||||
}),
|
||||
}))
|
||||
.await;
|
||||
|
||||
build_next_alarm(server, account_id, document_id, event)
|
||||
}
|
||||
|
||||
fn build_next_alarm(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
event: &ArchivedCalendarEvent,
|
||||
) -> trc::Result<TaskResult> {
|
||||
// Find next alarm time and write to task queue
|
||||
let now = now() as i64;
|
||||
if let Some(next_alarm) =
|
||||
event
|
||||
.data
|
||||
.next_alarm(now, Default::default())
|
||||
.and_then(|next_alarm| {
|
||||
// Verify minimum interval
|
||||
let max_next_alarm = now + server.core.groupware.alarms_minimum_interval;
|
||||
if next_alarm.alarm_time < max_next_alarm {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::AlarmSkipped),
|
||||
Reason = "Next alarm skipped due to minimum interval",
|
||||
Details = next_alarm.alarm_time - now,
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
event.data.next_alarm(max_next_alarm, Default::default())
|
||||
} else {
|
||||
Some(next_alarm)
|
||||
}
|
||||
})
|
||||
{
|
||||
Ok(TaskResult::Update(
|
||||
next_alarm.build_write_ops(account_id, document_id),
|
||||
))
|
||||
} else {
|
||||
Ok(TaskResult::Success(vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
struct Details {
|
||||
to: String,
|
||||
subject: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
async fn build_template(
|
||||
server: &Server,
|
||||
account_info: &AccountInfo,
|
||||
alarm: &TaskCalendarAlarmEmail,
|
||||
event: &ArchivedCalendarEvent,
|
||||
logo_cid: &str,
|
||||
) -> trc::Result<Option<Details>> {
|
||||
let account_id = alarm.account_id.document_id();
|
||||
let document_id = alarm.document_id.document_id();
|
||||
let (Some(event_component), Some(alarm_component)) = (
|
||||
event.data.event.components.get(alarm.event_id as usize),
|
||||
event.data.event.components.get(alarm.alarm_id as usize),
|
||||
) else {
|
||||
trc::event!(
|
||||
TaskManager(TaskManagerEvent::MetadataNotFound),
|
||||
Details = "Calendar Alarm component not found",
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Build webcal URI
|
||||
let webcal_uri = match event.webcal_uri(server, account_info).await {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.account_id(account_id)
|
||||
.document_id(document_id)
|
||||
.caused_by(trc::location!())
|
||||
.details("Failed to generate webcal URI")
|
||||
);
|
||||
String::from("#")
|
||||
}
|
||||
};
|
||||
|
||||
// Obtain alarm details
|
||||
let mut summary = None;
|
||||
let mut description = None;
|
||||
let mut rcpt_to = None;
|
||||
let mut location = None;
|
||||
let mut conference = None;
|
||||
let mut organizer = None;
|
||||
let mut guests = vec![];
|
||||
|
||||
for entry in alarm_component.entries.iter() {
|
||||
match &entry.name {
|
||||
ArchivedICalendarProperty::Summary => {
|
||||
summary = entry.values.first().and_then(|v| v.as_text());
|
||||
}
|
||||
ArchivedICalendarProperty::Description => {
|
||||
description = entry.values.first().and_then(|v| v.as_text());
|
||||
}
|
||||
ArchivedICalendarProperty::Attendee => {
|
||||
rcpt_to = entry
|
||||
.values
|
||||
.first()
|
||||
.and_then(|v| v.as_text())
|
||||
.map(strip_mailto_scheme)
|
||||
.and_then(sanitize_email);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in event_component.entries.iter() {
|
||||
match &entry.name {
|
||||
ArchivedICalendarProperty::Summary if summary.is_none() => {
|
||||
summary = entry.values.first().and_then(|v| v.as_text());
|
||||
}
|
||||
ArchivedICalendarProperty::Description if description.is_none() => {
|
||||
description = entry.values.first().and_then(|v| v.as_text());
|
||||
}
|
||||
ArchivedICalendarProperty::Location => {
|
||||
location = entry.values.first().and_then(|v| v.as_text());
|
||||
}
|
||||
ArchivedICalendarProperty::Conference if conference.is_none() => {
|
||||
conference = entry.values.first().and_then(|v| v.as_text());
|
||||
}
|
||||
ArchivedICalendarProperty::Organizer | ArchivedICalendarProperty::Attendee => {
|
||||
let email = entry
|
||||
.values
|
||||
.first()
|
||||
.and_then(|v| v.as_text())
|
||||
.map(strip_mailto_scheme);
|
||||
let name = entry.params.iter().find_map(|param| {
|
||||
if let ArchivedICalendarParameterName::Cn = param.name {
|
||||
param.value.as_text()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if email.is_some() || name.is_some() {
|
||||
if matches!(entry.name, ArchivedICalendarProperty::Organizer) {
|
||||
organizer = Some((email, name));
|
||||
} else {
|
||||
guests.push((email, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate recipient
|
||||
let rcpt_to = if let Some(rcpt_to) = rcpt_to {
|
||||
if server.core.groupware.alarms_allow_external_recipients
|
||||
|| account_info.addresses().contains(&rcpt_to)
|
||||
{
|
||||
rcpt_to
|
||||
} else {
|
||||
trc::event!(
|
||||
Calendar(trc::CalendarEvent::AlarmRecipientOverride),
|
||||
Reason = "External recipient not allowed for calendar alarms",
|
||||
Details = rcpt_to,
|
||||
AccountId = account_id,
|
||||
DocumentId = document_id,
|
||||
);
|
||||
|
||||
account_info.name().to_string()
|
||||
}
|
||||
} else {
|
||||
account_info.name().to_string()
|
||||
};
|
||||
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
let template = &server.core.groupware.alarms_template;
|
||||
let formatter = TextFormatter::new(account_info.locale().as_str())?;
|
||||
let locale = formatter.locale;
|
||||
|
||||
let start = formatter.field_to_string(
|
||||
&ItipValue::Time(ItipTime {
|
||||
start: alarm.event_start.timestamp(),
|
||||
tz_id: alarm.event_start_tz as u16,
|
||||
}),
|
||||
DateStyle::Short,
|
||||
);
|
||||
let end = formatter.field_to_string(
|
||||
&ItipValue::Time(ItipTime {
|
||||
start: alarm.event_end.timestamp(),
|
||||
tz_id: alarm.event_end_tz as u16,
|
||||
}),
|
||||
DateStyle::Short,
|
||||
);
|
||||
let subject = format!(
|
||||
"{}: {} @ {}",
|
||||
locale.calendar_alarm_subject_prefix,
|
||||
summary.or(description).unwrap_or("No Subject"),
|
||||
start
|
||||
);
|
||||
let organizer = organizer
|
||||
.map(|(email, name)| match (email, name) {
|
||||
(Some(email), Some(name)) => format!("{} <{}>", name, email),
|
||||
(Some(email), None) => email.to_string(),
|
||||
(None, Some(name)) => name.to_string(),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.unwrap_or_else(|| account_info.name().to_string());
|
||||
let logo_cid = format!("cid:{logo_cid}");
|
||||
let mut variables = Variables::new();
|
||||
variables.insert_single(CalendarTemplateVariable::PageTitle, subject.as_str());
|
||||
variables.insert_single(CalendarTemplateVariable::Lang, locale.name);
|
||||
variables.insert_single(CalendarTemplateVariable::Dir, locale.direction);
|
||||
variables.insert_single(
|
||||
CalendarTemplateVariable::Header,
|
||||
locale.calendar_alarm_header,
|
||||
);
|
||||
variables.insert_single(
|
||||
CalendarTemplateVariable::Footer,
|
||||
locale.calendar_alarm_footer,
|
||||
);
|
||||
variables.insert_single(
|
||||
CalendarTemplateVariable::ActionName,
|
||||
locale.calendar_alarm_open,
|
||||
);
|
||||
variables.insert_single(CalendarTemplateVariable::ActionUrl, webcal_uri.as_str());
|
||||
variables.insert_single(
|
||||
CalendarTemplateVariable::AttendeesTitle,
|
||||
locale.calendar_attendees,
|
||||
);
|
||||
if let Some(summary) = summary.filter(|summary| !summary.is_empty()) {
|
||||
variables.insert_single(CalendarTemplateVariable::EventTitle, summary);
|
||||
}
|
||||
variables.insert_single(CalendarTemplateVariable::LogoCid, logo_cid.as_str());
|
||||
if let Some(description) = description {
|
||||
variables.insert_single(CalendarTemplateVariable::EventDescription, description);
|
||||
}
|
||||
variables.insert_block(
|
||||
CalendarTemplateVariable::EventDetails,
|
||||
[
|
||||
Some(vec![
|
||||
(CalendarTemplateVariable::Key, locale.calendar_start),
|
||||
(CalendarTemplateVariable::Value, start.as_str()),
|
||||
]),
|
||||
Some(vec![
|
||||
(CalendarTemplateVariable::Key, locale.calendar_end),
|
||||
(CalendarTemplateVariable::Value, end.as_str()),
|
||||
]),
|
||||
location.map(|location| {
|
||||
vec![
|
||||
(CalendarTemplateVariable::Key, locale.calendar_location),
|
||||
(CalendarTemplateVariable::Value, location),
|
||||
]
|
||||
}),
|
||||
conference.map(|conference| {
|
||||
let mut detail = vec![
|
||||
(CalendarTemplateVariable::Key, locale.calendar_conference),
|
||||
(CalendarTemplateVariable::Value, conference),
|
||||
];
|
||||
if let Some(link) = hyperlink(conference) {
|
||||
detail.push((CalendarTemplateVariable::Link, link));
|
||||
}
|
||||
detail
|
||||
}),
|
||||
Some(vec![
|
||||
(CalendarTemplateVariable::Key, locale.calendar_organizer),
|
||||
(CalendarTemplateVariable::Value, organizer.as_str()),
|
||||
]),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
if !guests.is_empty() {
|
||||
variables.insert_block(
|
||||
CalendarTemplateVariable::Attendees,
|
||||
guests.into_iter().map(|(email, name)| {
|
||||
[
|
||||
(CalendarTemplateVariable::Key, name.unwrap_or_default()),
|
||||
(CalendarTemplateVariable::Value, email.unwrap_or_default()),
|
||||
]
|
||||
}),
|
||||
);
|
||||
}
|
||||
Ok(Some(Details {
|
||||
to: rcpt_to,
|
||||
body: template.eval(&variables),
|
||||
subject,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user