1use crate::common::{
8 Server,
9 auth::{AccessToken, ResourceToken},
10 storage::index::ObjectIndexBuilder,
11};
12use crate::email::sieve::{
13 ArchivedSieveScript, SieveScript, delete::SieveScriptDelete, ingest::SieveScriptIngest,
14};
15use crate::http_proto::HttpSessionData;
16use crate::jmap::{blob::download::BlobDownload, changes::state::StateManager};
17use crate::jmap_proto::{
18 error::set::{SetError, SetErrorType},
19 method::set::{SetRequest, SetResponse},
20 object::sieve::{Sieve, SieveProperty, SieveValue},
21 references::resolve::ResolveCreatedReference,
22 request::{IntoValid, reference::MaybeIdReference},
23 types::state::State,
24};
25use crate::store::{
26 Serialize, SerializeInfallible, ValueKey,
27 rand::{Rng, rng},
28 write::{AlignedBytes, Archive, Archiver, BatchBuilder},
29};
30use crate::trc::AddContext;
31use crate::types::{
32 blob::{BlobClass, BlobId, BlobSection},
33 collection::{Collection, SyncCollection},
34 field::{PrincipalField, SieveField},
35 id::Id,
36};
37use jmap_tools::{Key, Map, Value};
38use rand::distr::Alphanumeric;
39use sieve::compiler::ErrorType;
40use std::future::Future;
41
42pub struct SetContext<'x> {
43 resource_token: ResourceToken,
44 access_token: &'x AccessToken,
45 response: SetResponse<Sieve>,
46}
47
48pub trait SieveScriptSet: Sync + Send {
49 fn sieve_script_set(
50 &self,
51 request: SetRequest<'_, Sieve>,
52 access_token: &AccessToken,
53 session: &HttpSessionData,
54 ) -> impl Future<Output = crate::trc::Result<SetResponse<Sieve>>> + Send;
55
56 #[allow(clippy::type_complexity)]
57 fn sieve_set_item<'x>(
58 &self,
59 changes_: Value<'_, SieveProperty, SieveValue>,
60 update: Option<(u32, Archive<&'x ArchivedSieveScript>)>,
61 ctx: &SetContext,
62 session_id: u64,
63 ) -> impl Future<
64 Output = crate::trc::Result<
65 Result<
66 (
67 ObjectIndexBuilder<&'x ArchivedSieveScript, SieveScript>,
68 Option<Vec<u8>>,
69 ),
70 SetError<SieveProperty>,
71 >,
72 >,
73 > + Send;
74}
75
76impl SieveScriptSet for Server {
77 async fn sieve_script_set(
78 &self,
79 mut request: SetRequest<'_, Sieve>,
80 access_token: &AccessToken,
81 session: &HttpSessionData,
82 ) -> crate::trc::Result<SetResponse<Sieve>> {
83 let account_id = request.account_id.document_id();
84 let sieve_ids = self
85 .document_ids(account_id, Collection::SieveScript, SieveField::Name)
86 .await?;
87 let mut ctx = SetContext {
88 resource_token: self.get_resource_token(access_token, account_id).await?,
89 access_token,
90 response: SetResponse::from_request(&request, self.core.jmap.set_max_objects)?
91 .with_state(
92 self.assert_state(
93 account_id,
94 SyncCollection::SieveScript,
95 &request.if_in_state,
96 )
97 .await?,
98 ),
99 };
100 let will_destroy = request.unwrap_destroy().into_valid().collect::<Vec<_>>();
101
102 if let Some(MaybeIdReference::Id(id)) = &request.arguments.on_success_activate_script
104 && !sieve_ids.contains(id.document_id())
105 {
106 request.arguments.on_success_activate_script = None;
107 }
108
109 let mut batch = BatchBuilder::new();
111 for (id, object) in request.unwrap_create() {
112 if sieve_ids.len() < access_token.object_quota(Collection::SieveScript) as u64 {
113 match self
114 .sieve_set_item(object, None, &ctx, session.session_id)
115 .await?
116 {
117 Ok((mut builder, Some(blob))) => {
118 let sieve = &mut builder.changes_mut().unwrap();
120 let (blob_hash, blob_hold) =
121 self.put_temporary_blob(account_id, &blob, 60).await?;
122 sieve.blob_hash = blob_hash;
123 let blob_size = sieve.size as usize;
124 let blob_hash = sieve.blob_hash.clone();
125
126 let document_id = self
128 .store()
129 .assign_document_ids(account_id, Collection::SieveScript, 1)
130 .await
131 .caused_by(crate::trc::location!())?;
132 batch
133 .with_account_id(account_id)
134 .with_collection(Collection::SieveScript)
135 .with_document(document_id)
136 .custom(builder.with_access_token(ctx.access_token))
137 .caused_by(crate::trc::location!())?
138 .clear(blob_hold)
139 .commit_point();
140
141 let mut result = Map::with_capacity(1)
142 .with_key_value(SieveProperty::Id, SieveValue::Id(document_id.into()))
143 .with_key_value(
144 SieveProperty::BlobId,
145 SieveValue::BlobId(BlobId {
146 hash: blob_hash,
147 class: BlobClass::Linked {
148 account_id,
149 collection: Collection::SieveScript.into(),
150 document_id,
151 },
152 section: BlobSection {
153 size: blob_size,
154 ..Default::default()
155 }
156 .into(),
157 }),
158 );
159
160 if let Some(MaybeIdReference::Reference(id_ref)) =
162 &request.arguments.on_success_activate_script
163 && id_ref == &id
164 {
165 request.arguments.on_success_activate_script =
166 Some(MaybeIdReference::Id(Id::from(document_id)));
167 result.insert_unchecked(SieveProperty::IsActive, true);
168 }
169
170 ctx.response.created.insert(id, result.into());
172 }
173 Err(err) => {
174 ctx.response.not_created.append(id, err);
175 }
176 _ => unreachable!(),
177 }
178 } else {
179 ctx.response.not_created.append(
180 id,
181 SetError::new(SetErrorType::OverQuota).with_description(concat!(
182 "There are too many sieve scripts, ",
183 "please delete some before adding a new one."
184 )),
185 );
186 }
187 }
188
189 'update: for (id, object) in request.unwrap_update().into_valid() {
191 if will_destroy.contains(&id) {
193 ctx.response
194 .not_updated
195 .append(id, SetError::will_destroy());
196 continue 'update;
197 }
198
199 let document_id = id.document_id();
201 if let Some(sieve_) = self
202 .store()
203 .get_value::<Archive<AlignedBytes>>(ValueKey::archive(
204 account_id,
205 Collection::SieveScript,
206 document_id,
207 ))
208 .await?
209 {
210 let sieve = sieve_
211 .to_unarchived::<SieveScript>()
212 .caused_by(crate::trc::location!())?;
213
214 match self
215 .sieve_set_item(
216 object,
217 (document_id, sieve).into(),
218 &ctx,
219 session.session_id,
220 )
221 .await?
222 {
223 Ok((mut builder, blob)) => {
224 batch
226 .with_account_id(account_id)
227 .with_collection(Collection::SieveScript)
228 .with_document(document_id);
229
230 let blob_id = if let Some(blob) = blob {
231 let sieve = &mut builder.changes_mut().unwrap();
233 let (blob_hash, blob_hold) =
234 self.put_temporary_blob(account_id, &blob, 60).await?;
235 sieve.blob_hash = blob_hash;
236 batch.clear(blob_hold);
237
238 BlobId {
239 hash: sieve.blob_hash.clone(),
240 class: BlobClass::Linked {
241 account_id,
242 collection: Collection::SieveScript.into(),
243 document_id,
244 },
245 section: BlobSection {
246 size: sieve.size as usize,
247 ..Default::default()
248 }
249 .into(),
250 }
251 .into()
252 } else {
253 None
254 };
255
256 batch
258 .custom(builder.with_access_token(ctx.access_token))
259 .caused_by(crate::trc::location!())?
260 .commit_point();
261
262 let mut result = Map::with_capacity(1);
264 if let Some(blob_id) = blob_id {
265 result.insert_unchecked(
266 SieveProperty::BlobId,
267 SieveValue::BlobId(blob_id),
268 );
269 }
270
271 if let Some(MaybeIdReference::Id(id)) =
273 &request.arguments.on_success_activate_script
274 && document_id == id.document_id()
275 {
276 result.insert_unchecked(SieveProperty::IsActive, true);
277 }
278
279 ctx.response.updated.append(
281 id,
282 if !result.is_empty() {
283 Value::Object(result).into()
284 } else {
285 None
286 },
287 );
288 }
289 Err(err) => {
290 ctx.response.not_updated.append(id, err);
291 continue 'update;
292 }
293 }
294 } else {
295 ctx.response.not_updated.append(id, SetError::not_found());
296 }
297 }
298
299 let active_script_id = self.sieve_script_get_active_id(account_id).await?;
301 for id in will_destroy {
302 let document_id = id.document_id();
303 if sieve_ids.contains(document_id) {
304 if active_script_id != Some(document_id) {
305 if self
306 .sieve_script_delete(account_id, document_id, ctx.access_token, &mut batch)
307 .await?
308 {
309 ctx.response.destroyed.push(id);
310 } else {
311 ctx.response.not_destroyed.append(id, SetError::not_found());
312 }
313 } else {
314 ctx.response.not_destroyed.append(
315 id,
316 SetError::new(SetErrorType::ScriptIsActive)
317 .with_description("Deactivate Sieve script before deletion."),
318 );
319 }
320 } else {
321 ctx.response.not_destroyed.append(id, SetError::not_found());
322 }
323 }
324
325 let on_success_deactivate_script = request
327 .arguments
328 .on_success_deactivate_script
329 .unwrap_or(false);
330 if ctx.response.not_created.is_empty()
331 && ctx.response.not_updated.is_empty()
332 && ctx.response.not_destroyed.is_empty()
333 && (request.arguments.on_success_activate_script.is_some()
334 || on_success_deactivate_script)
335 {
336 if let Some(MaybeIdReference::Id(id)) = request.arguments.on_success_activate_script {
337 batch
338 .with_account_id(account_id)
339 .with_collection(Collection::Principal)
340 .with_document(0)
341 .set(PrincipalField::ActiveScriptId, id.document_id().serialize());
342 } else if on_success_deactivate_script {
343 batch
344 .with_account_id(account_id)
345 .with_collection(Collection::Principal)
346 .with_document(0)
347 .clear(PrincipalField::ActiveScriptId);
348 }
349 }
350
351 if !batch.is_empty()
353 && let Ok(change_id) = self
354 .commit_batch(batch)
355 .await
356 .caused_by(crate::trc::location!())?
357 .last_change_id(account_id)
358 {
359 ctx.response.new_state = State::Exact(change_id).into();
360 }
361
362 Ok(ctx.response)
363 }
364
365 #[allow(clippy::blocks_in_conditions)]
366 async fn sieve_set_item<'x>(
367 &self,
368 changes_: Value<'_, SieveProperty, SieveValue>,
369 update: Option<(u32, Archive<&'x ArchivedSieveScript>)>,
370 ctx: &SetContext<'_>,
371 session_id: u64,
372 ) -> crate::trc::Result<
373 Result<
374 (
375 ObjectIndexBuilder<&'x ArchivedSieveScript, SieveScript>,
376 Option<Vec<u8>>,
377 ),
378 SetError<SieveProperty>,
379 >,
380 > {
381 if update
383 .as_ref()
384 .is_some_and(|(_, obj)| obj.inner.name.eq_ignore_ascii_case("vacation"))
385 {
386 return Ok(Err(SetError::forbidden().with_description(concat!(
387 "The 'vacation' script cannot be modified, ",
388 "use VacationResponse/set instead."
389 ))));
390 }
391
392 let mut changes = update
394 .as_ref()
395 .map(|(_, obj)| obj.deserialize().unwrap_or_default())
396 .unwrap_or_default();
397 let mut blob_id = None;
398 for (property, mut value) in changes_.into_expanded_object() {
399 if let Err(err) = ctx.response.resolve_self_references(&mut value) {
400 return Ok(Err(err));
401 };
402 match (&property, value) {
403 (Key::Property(SieveProperty::Name), Value::Str(value)) => {
404 if value.len() > self.core.jmap.sieve_max_script_name {
405 return Ok(Err(SetError::invalid_properties()
406 .with_property(property.into_owned())
407 .with_description("Script name is too long.")));
408 } else if value.eq_ignore_ascii_case("vacation") {
409 return Ok(Err(SetError::forbidden()
410 .with_property(property.into_owned())
411 .with_description(
412 "The 'vacation' name is reserved, please use a different name.",
413 )));
414 } else if update
415 .as_ref()
416 .is_none_or(|(_, obj)| obj.inner.name != value.as_ref())
417 && let Some(id) = self
418 .document_ids_matching(
419 ctx.resource_token.account_id,
420 Collection::SieveScript,
421 SieveField::Name,
422 value.as_bytes(),
423 )
424 .await?
425 .min()
426 {
427 return Ok(Err(SetError::already_exists()
428 .with_existing_id(id.into())
429 .with_description(format!(
430 "A sieve script with name '{}' already exists.",
431 value
432 ))));
433 }
434
435 changes.name = value.into_owned();
436 }
437 (
438 Key::Property(SieveProperty::BlobId),
439 Value::Element(SieveValue::BlobId(value)),
440 ) => {
441 blob_id = value.into();
442 continue;
443 }
444 (Key::Property(SieveProperty::Name), Value::Null) => {
445 continue;
446 }
447 _ => {
448 return Ok(Err(SetError::invalid_properties()
449 .with_property(property.into_owned())
450 .with_description("Invalid property or value.".to_string())));
451 }
452 }
453 }
454
455 if update.is_none() {
456 if changes.name.is_empty() {
458 changes.name = rng()
459 .sample_iter(Alphanumeric)
460 .take(15)
461 .map(char::from)
462 .collect::<String>();
463 }
464 }
465
466 let blob_update = if let Some(blob_id) = blob_id {
467 if update.as_ref().is_none_or( |(document_id, _)| {
468 !matches!(blob_id.class, BlobClass::Linked { account_id, collection, document_id: d } if account_id == ctx.resource_token.account_id && collection == u8::from(Collection::SieveScript) && *document_id == d)
469 }) {
470 if let Some(mut bytes) = self.blob_download(&blob_id, ctx.access_token).await? {
472 match self
474 .has_available_quota(&ctx.resource_token, bytes.len() as u64)
475 .await
476 {
477 Ok(_) => (),
478 Err(err) => {
479 if err.matches(crate::trc::EventType::Limit(crate::trc::LimitEvent::Quota))
480 || err.matches(crate::trc::EventType::Limit(crate::trc::LimitEvent::TenantQuota))
481 {
482 crate::trc::error!(err.account_id(ctx.resource_token.account_id).span_id(session_id));
483 return Ok(Err(SetError::over_quota()));
484 } else {
485 return Err(err);
486 }
487 }
488 }
489
490 match self.core.sieve.untrusted_compiler.compile(&bytes) {
492 Ok(script) => {
493 changes.size = bytes.len() as u32;
494 bytes.extend(Archiver::new(script).untrusted().serialize().caused_by(crate::trc::location!())?);
495 bytes.into()
496 }
497 Err(err) => {
498 return Ok(Err(SetError::new(
499 if let ErrorType::ScriptTooLong = &err.error_type() {
500 SetErrorType::TooLarge
501 } else {
502 SetErrorType::InvalidScript
503 },
504 )
505 .with_description(err.to_string())));
506 }
507 }
508 } else {
509 return Ok(Err(SetError::new(SetErrorType::BlobNotFound)
510 .with_property(SieveProperty::BlobId)
511 .with_description("Blob does not exist.")));
512 }
513 } else {
514 None
515 }
516 } else if update.is_none() {
517 return Ok(Err(SetError::invalid_properties()
518 .with_property(SieveProperty::BlobId)
519 .with_description("Missing blobId.")));
520 } else {
521 None
522 };
523
524 Ok(Ok((
526 ObjectIndexBuilder::new()
527 .with_changes(changes)
528 .with_current_opt(update.map(|(_, current)| current)),
529 blob_update,
530 )))
531 }
532}