Skip to main content

stalwart_lib/jmap/src/sieve/
set.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use 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        // Validate active script id
103        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        // Process creates
110        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                        // Store blob
119                        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                        // Write record
127                        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                        // Update active script if needed
161                        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                        // Add result with updated blobId
171                        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        // Process updates
190        'update: for (id, object) in request.unwrap_update().into_valid() {
191            // Make sure id won't be destroyed
192            if will_destroy.contains(&id) {
193                ctx.response
194                    .not_updated
195                    .append(id, SetError::will_destroy());
196                continue 'update;
197            }
198
199            // Obtain sieve script
200            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                        // Prepare write batch
225                        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                            // Store blob
232                            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                        // Write record
257                        batch
258                            .custom(builder.with_access_token(ctx.access_token))
259                            .caused_by(crate::trc::location!())?
260                            .commit_point();
261
262                        // Update blobId property if needed
263                        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                        // Add active script property if needed
272                        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                        // Add result
280                        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        // Process deletions
300        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        // Activate / deactivate scripts
326        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        // Write changes
352        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        // Vacation script cannot be modified
382        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        // Parse properties
393        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            // Add name if missing
457            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                // Check access
471                if let Some(mut bytes) = self.blob_download(&blob_id, ctx.access_token).await? {
472                    // Check quota
473                    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                    // Compile script
491                    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        // Validate
525        Ok(Ok((
526            ObjectIndexBuilder::new()
527                .with_changes(changes)
528                .with_current_opt(update.map(|(_, current)| current)),
529            blob_update,
530        )))
531    }
532}