Files
Stalwart/crates/jmap/src/vacation/set.rs
2026-06-18 16:27:27 +02:00

458 lines
17 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::get::VacationResponseGet;
use crate::changes::state::StateManager;
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use email::sieve::{SieveScript, VacationResponse, ingest::SieveScriptIngest};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse},
object::vacation_response::{self, VacationResponseProperty, VacationResponseValue},
references::resolve::ResolveCreatedReference,
request::IntoValid,
types::date::UTCDate,
};
use jmap_tools::{Key, Map, Value};
use mail_builder::MessageBuilder;
use mail_parser::decoders::html::html_to_text;
use std::borrow::Cow;
use std::future::Future;
use store::{
Serialize, SerializeInfallible, ValueKey,
write::{AlignedBytes, Archive, Archiver, BatchBuilder},
};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
field::PrincipalField,
id::Id,
};
pub trait VacationResponseSet: Sync + Send {
fn vacation_response_set(
&self,
request: SetRequest<'_, vacation_response::VacationResponse>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<SetResponse<vacation_response::VacationResponse>>> + Send;
fn build_script(&self, obj: &mut SieveScript) -> trc::Result<Vec<u8>>;
}
impl VacationResponseSet for Server {
async fn vacation_response_set(
&self,
mut request: SetRequest<'_, vacation_response::VacationResponse>,
access_token: &AccessToken,
) -> trc::Result<SetResponse<vacation_response::VacationResponse>> {
let account_id = request.account_id.document_id();
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
.with_state(
self.assert_state(
account_id,
SyncCollection::SieveScript,
&request.if_in_state,
)
.await?,
);
for id in request.unwrap_destroy().into_valid() {
response.not_destroyed.append(id, SetError::singleton());
}
// Process set or update requests
let create_id = None;
let mut changes = None;
match (request.create, request.update) {
(Some(create), Some(update)) if !create.is_empty() && !update.is_empty() => {
return Err(trc::JmapEvent::InvalidArguments
.into_err()
.details("Creating and updating on the same request is not allowed."));
}
(Some(create), _) if !create.is_empty() => {
for (id, _obj) in create {
response.not_created.append(id, SetError::singleton());
}
}
(_, Some(update)) if !update.is_empty() => {
for (id, obj) in update.into_valid() {
if id.is_singleton() {
changes = Some(obj);
} else {
response.not_updated.append(
id,
SetError::new(SetErrorType::NotFound).with_description("ID not found."),
);
}
}
}
_ => {
return Ok(response);
}
}
// Prepare write batch
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::SieveScript);
// Process changes
let active_script_id = self.sieve_script_get_active_id(account_id).await?;
if let Some(changes) = changes {
// Obtain current script
let document_id = self.get_vacation_sieve_script_id(account_id).await?;
let (mut sieve, prev_sieve) = if let Some(document_id) = document_id {
let prev_sieve = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::SieveScript,
document_id,
))
.await?
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
})?
.into_deserialized::<SieveScript>()
.caused_by(trc::location!())?;
let mut sieve = prev_sieve.inner.clone();
if sieve.vacation_response.is_none() {
sieve.vacation_response = VacationResponse::default().into();
}
(sieve, Some(prev_sieve))
} else {
(
SieveScript {
name: "vacation".into(),
blob_hash: Default::default(),
size: 0,
vacation_response: VacationResponse::default().into(),
},
None,
)
};
// Parse properties
let mut is_active = false;
let mut build_script = create_id.is_some();
let vacation = sieve.vacation_response.as_mut().unwrap();
for (property, mut value) in changes.into_expanded_object() {
if let Err(err) = response.resolve_self_references(&mut value, 0, false) {
return Ok(set_error(response, create_id, err));
};
match (&property, value) {
(Key::Property(VacationResponseProperty::Subject), Value::Str(value))
if value.len() < 512 =>
{
build_script = true;
vacation.subject = Some(value.into_owned());
}
(Key::Property(VacationResponseProperty::HtmlBody), Value::Str(value))
if value.len() < 2048 =>
{
build_script = true;
vacation.html_body = Some(value.into_owned());
}
(Key::Property(VacationResponseProperty::TextBody), Value::Str(value))
if value.len() < 2048 =>
{
build_script = true;
vacation.text_body = Some(value.into_owned());
}
(
Key::Property(VacationResponseProperty::FromDate),
Value::Element(VacationResponseValue::Date(date)),
) => {
vacation.from_date = Some(date.timestamp() as u64);
build_script = true;
}
(
Key::Property(VacationResponseProperty::ToDate),
Value::Element(VacationResponseValue::Date(date)),
) => {
vacation.to_date = Some(date.timestamp() as u64);
build_script = true;
}
(Key::Property(VacationResponseProperty::IsEnabled), Value::Bool(value)) => {
is_active = value;
}
(Key::Property(VacationResponseProperty::IsEnabled), Value::Null) => {
is_active = false;
}
(
Key::Property(
VacationResponseProperty::Subject
| VacationResponseProperty::HtmlBody
| VacationResponseProperty::TextBody
| VacationResponseProperty::ToDate
| VacationResponseProperty::FromDate,
),
Value::Null,
) => {
if create_id.is_none() {
build_script = true;
match property {
Key::Property(VacationResponseProperty::Subject) => {
vacation.subject = None;
}
Key::Property(VacationResponseProperty::HtmlBody) => {
vacation.html_body = None;
}
Key::Property(VacationResponseProperty::TextBody) => {
vacation.text_body = None;
}
Key::Property(VacationResponseProperty::FromDate) => {
vacation.from_date = None;
}
Key::Property(VacationResponseProperty::ToDate) => {
vacation.to_date = None;
}
_ => unreachable!(),
}
}
}
(Key::Property(VacationResponseProperty::Id), value) => {
if create_id.is_some() || !crate::matches_id(&value, Id::singleton()) {
return Ok(set_error(
response,
create_id,
SetError::invalid_properties()
.with_property(VacationResponseProperty::Id)
.with_description("The id property is immutable."),
));
}
}
_ => {
return Ok(set_error(
response,
create_id,
SetError::invalid_properties()
.with_property(property.into_owned())
.with_description("Field could not be set."),
));
}
}
}
let mut obj = ObjectIndexBuilder::new()
.with_current_opt(prev_sieve)
.with_changes(sieve)
.with_changed_by(access_token.account_tenant_ids());
// Update id
let document_id = if let Some(document_id) = document_id {
batch.with_document(document_id);
document_id
} else {
let document_id = self
.store()
.assign_document_ids(account_id, Collection::SieveScript, 1)
.await
.caused_by(trc::location!())?;
batch.with_document(document_id);
document_id
};
// Create sieve script only if there are changes
if build_script {
// Upload new blob
let (blob_hash, blob_hold) = self
.put_temporary_blob(
account_id,
&self.build_script(obj.changes_mut().unwrap())?,
60,
)
.await?;
obj.changes_mut().unwrap().blob_hash = blob_hash;
batch.clear(blob_hold);
};
batch.custom(obj).caused_by(trc::location!())?;
// Deactivate other sieve scripts
let was_active = active_script_id == Some(document_id);
if is_active {
if !was_active {
batch
.with_collection(Collection::Principal)
.with_document(0)
.set(PrincipalField::ActiveScriptId, document_id.serialize());
}
} else if was_active {
batch
.with_collection(Collection::Principal)
.with_document(0)
.clear(PrincipalField::ActiveScriptId);
}
// Write changes
if !batch.is_empty() {
response.new_state = Some(
self.commit_batch(batch)
.await
.and_then(|ids| ids.last_change_id(account_id))
.caused_by(trc::location!())?
.into(),
);
}
// Add result
if let Some(create_id) = create_id {
response.created.insert(
create_id,
Map::with_capacity(1)
.with_key_value(VacationResponseProperty::Id, Id::singleton())
.into(),
);
} else {
response.updated.append(Id::singleton(), None);
}
}
Ok(response)
}
fn build_script(&self, obj: &mut SieveScript) -> trc::Result<Vec<u8>> {
// Build Sieve script
let mut script = Vec::with_capacity(1024);
script.extend_from_slice(b"require [\"vacation\", \"relational\", \"date\"];\r\n\r\n");
let mut num_blocks = 0;
// Add start date
if let Some(value) = obj.vacation_response.as_ref().and_then(|v| v.from_date) {
script.extend_from_slice(b"if currentdate :value \"ge\" \"iso8601\" \"");
script.extend_from_slice(UTCDate::from(value).to_string().as_bytes());
script.extend_from_slice(b"\" {\r\n");
num_blocks += 1;
}
// Add end date
if let Some(value) = obj.vacation_response.as_ref().and_then(|v| v.to_date) {
script.extend_from_slice(b"if currentdate :value \"le\" \"iso8601\" \"");
script.extend_from_slice(UTCDate::from(value).to_string().as_bytes());
script.extend_from_slice(b"\" {\r\n");
num_blocks += 1;
}
script.extend_from_slice(b"vacation :mime ");
if let Some(value) = obj
.vacation_response
.as_ref()
.and_then(|v| v.subject.as_ref())
{
script.extend_from_slice(b":subject \"");
for &ch in value.as_bytes().iter() {
match ch {
b'\\' | b'\"' => {
script.push(b'\\');
}
b'\r' | b'\n' => {
continue;
}
_ => (),
}
script.push(ch);
}
script.extend_from_slice(b"\" ");
}
let mut text_body = if let Some(value) = obj
.vacation_response
.as_ref()
.and_then(|v| v.text_body.as_ref())
{
Cow::from(value.as_str()).into()
} else {
None
};
let html_body = if let Some(value) = obj
.vacation_response
.as_ref()
.and_then(|v| v.html_body.as_ref())
{
Cow::from(value.as_str()).into()
} else {
None
};
match (&html_body, &text_body) {
(Some(html_body), None) => {
text_body = Cow::from(html_to_text(html_body.as_ref())).into();
}
(None, None) => {
text_body = Cow::from("I am away.").into();
}
_ => (),
}
let mut builder = MessageBuilder::new();
let mut body_len = 0;
if let Some(html_body) = html_body {
body_len = html_body.len();
builder = builder.html_body(html_body);
}
if let Some(text_body) = text_body {
body_len += text_body.len();
builder = builder.text_body(text_body);
}
let mut message_body = Vec::with_capacity(body_len + 128);
builder.write_body(&mut message_body).ok();
script.push(b'\"');
for ch in message_body {
if [b'\\', b'\"'].contains(&ch) {
script.push(b'\\');
}
script.push(ch);
}
script.extend_from_slice(b"\";\r\n");
// Close blocks
for _ in 0..num_blocks {
script.extend_from_slice(b"}\r\n");
}
match self.core.sieve.untrusted_compiler.compile(&script) {
Ok(compiled_script) => {
// Update blob length
obj.size = script.len() as u32;
// Serialize script
script.extend(
Archiver::new(compiled_script)
.untrusted()
.serialize()
.caused_by(trc::location!())?,
);
Ok(script)
}
Err(err) => Err(trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.reason(err)
.details("Vacation Sieve Script failed to compile.")),
}
}
}
fn set_error(
mut response: SetResponse<vacation_response::VacationResponse>,
id: Option<String>,
err: SetError<VacationResponseProperty>,
) -> SetResponse<vacation_response::VacationResponse> {
if let Some(id) = id {
response.not_created.append(id, err);
} else {
response.not_updated.append(Id::singleton(), err);
}
response
}