diff --git a/Cargo.lock b/Cargo.lock index ed5d81c..4a690a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7958,8 +7958,6 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "sieve-rs" version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd00a548fde57bd0c8e7c13ae65fc5fe30bd923ff8655c81e010f3f02a90997b" dependencies = [ "ahash", "arc-swap", @@ -8590,6 +8588,7 @@ dependencies = [ "mail-builder 1.0.0", "mail-parser", "managesieve", + "migration", "nlp", "pop3", "quick-xml 0.41.0", diff --git a/Cargo.toml b/Cargo.toml index 55c2b2f..a2d211f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,7 @@ [workspace] resolver = "2" +# Vendored crates are patched in below, not built as members. +exclude = ["vendor"] members = [ "crates/main", "crates/types", @@ -78,3 +80,10 @@ incremental = false debug-assertions = false overflow-checks = false rpath = false + +# inbuxa: sieve-rs spells upstream's name into its Sieve extension names +# (vnd.stalwart.*), which scripts `require` and ManageSieve advertises. +# vendor/sieve-rs is the published 0.7.3 with those renamed; see its +# VENDORED.md. Re-vendor when the version in Cargo.lock moves. +[patch.crates-io] +sieve-rs = { path = "vendor/sieve-rs" } diff --git a/README.md b/README.md index a0570d4..f56a8d5 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ docker build -t inbuxa . # or the container image ``` Settings are read from `INBUXA_*` environment variables. An existing Stalwart -install's `STALWART_*` variables still work, with a warning to rename them. +install's `STALWART_*` variables aren't read: the server stops at startup and +names each one to rename. New installs keep their data in `/var/lib/inbuxa` and logs in `/var/log/inbuxa`. Existing installs keep the paths their configuration already names, so none of their data moves. diff --git a/api/v1/openapi.yml b/api/v1/openapi.yml index 7de8b44..bc1cd1d 100644 --- a/api/v1/openapi.yml +++ b/api/v1/openapi.yml @@ -1,8 +1,8 @@ openapi: 3.0.3 info: - title: Stalwart Management API + title: inbuxa Management API description: | - REST Management API for Stalwart server. These endpoints are helpers + REST Management API for the inbuxa server. These endpoints are helpers that complement the JMAP API — most of the server's configuration and data is managed via JMAP (see `POST /jmap/`). The endpoints documented here cover interactive login, account introspection, configuration schema retrieval and @@ -12,11 +12,11 @@ info: name: AGPL-3.0-only OR LicenseRef-SEL servers: - url: https://{host} - description: Stalwart server + description: inbuxa server variables: host: default: mail.example.com - description: The hostname of Stalwart server + description: The hostname of the inbuxa server security: - bearerAuth: [] - basicAuth: [] @@ -154,7 +154,7 @@ paths: operationId: getSchema summary: Return the configuration schema at a specific hash description: | - Returns the JSON Schema describing the full Stalwart configuration tree. + Returns the JSON Schema describing the full inbuxa configuration tree. The response is always gzip-encoded (`Content-Encoding: gzip`) and served with an immutable cache policy — the schema for a given hash never changes. If the hash does not match the server's current schema, the @@ -183,7 +183,7 @@ paths: application/json: schema: type: object - description: JSON Schema document describing Stalwart config + description: JSON Schema document describing inbuxa config additionalProperties: true '302': description: Redirect to the current schema URL when the hash is stale @@ -395,7 +395,7 @@ components: WWW-Authenticate: schema: type: string - example: Bearer realm="Stalwart Server" + example: Bearer realm="INBUXA Server" content: application/problem+json: schema: diff --git a/crates/common/src/config/mailstore/scripts.rs b/crates/common/src/config/mailstore/scripts.rs index f85a812..4067603 100644 --- a/crates/common/src/config/mailstore/scripts.rs +++ b/crates/common/src/config/mailstore/scripts.rs @@ -143,7 +143,9 @@ impl Scripting { .with_cpu_limit(trusted.max_cpu_cycles as usize) .with_max_nested_includes(trusted.max_nested_includes as usize) .with_max_received_headers(trusted.max_received_headers as usize) - .with_default_duplicate_expiry(trusted.duplicate_expiry.into_inner().as_secs()); + .with_default_duplicate_expiry(trusted.duplicate_expiry.into_inner().as_secs()) + // inbuxa: without it, `environment "name"` answers sieve-rs's default + .with_env_variable("name", types::brand_server!()); trusted_runtime.set_local_hostname(local_hostname.clone()); untrusted_runtime.set_local_hostname(local_hostname); @@ -279,3 +281,23 @@ impl Clone for Scripting { } } } + +#[cfg(test)] +mod tests { + use sieve::compiler::grammar::Capability; + + // inbuxa: sieve-rs is vendored (vendor/sieve-rs) to carry the fork's + // name in its Sieve extensions. If Cargo.lock moves sieve-rs past the + // vendored version, Cargo drops the patch with only a warning and + // upstream's spelling comes back; this fails instead. + #[test] + fn sieve_extensions_carry_the_fork_name() { + for (capability, name) in [ + (Capability::While, "vnd.inbuxa.while"), + (Capability::Expressions, "vnd.inbuxa.expressions"), + ] { + assert_eq!(capability.to_string(), name); + assert_eq!(Capability::parse(name), capability); + } + } +} diff --git a/crates/common/src/manager/application.rs b/crates/common/src/manager/application.rs index b3eb6a3..27a0f03 100644 --- a/crates/common/src/manager/application.rs +++ b/crates/common/src/manager/application.rs @@ -514,12 +514,12 @@ mod tests { #[test] fn index_is_rewritten_with_the_prefix_and_client_id() { - let meta = oauth_client_id_meta("stalwart-webui"); + let meta = oauth_client_id_meta("inbuxa-webui"); let html = String::from_utf8(rewrite_index(INDEX, "admin", Some(&meta))).unwrap(); assert!(html.contains(""), "{html}"); assert!( - html.contains(""), + html.contains(""), "{html}" ); assert!(html.contains("Portal"), "{html}"); @@ -541,7 +541,7 @@ mod tests { #[test] fn index_without_a_placeholder_is_left_alone() { let bundle = "\n \n"; - let meta = oauth_client_id_meta("stalwart-webui"); + let meta = oauth_client_id_meta("inbuxa-webui"); let html = String::from_utf8(rewrite_index(bundle, "admin", Some(&meta))).unwrap(); assert_eq!(html, "\n \n"); diff --git a/crates/common/src/manager/first_party.rs b/crates/common/src/manager/first_party.rs index 70ed0bd..a9c6f0a 100644 --- a/crates/common/src/manager/first_party.rs +++ b/crates/common/src/manager/first_party.rs @@ -10,7 +10,7 @@ //! that ship with it are registered for it, on every start: //! //! - the web interface the server serves itself (`Application`, `/admin` and -//! `/account`), as its OAuth client id, `stalwart-webui` unless the +//! `/account`), as its OAuth client id, `inbuxa-webui` unless the //! application names another; //! - INBUXA Admin hosted elsewhere, as `inbuxa-admin`, when `INBUXA_ADMIN_URL` //! is set; @@ -29,7 +29,7 @@ use directory::core::secret::{hash_secret, verify_secret_hash}; use registry::{ schema::{ enums::{PasswordHashAlgorithm, ServiceProtocol}, - prelude::{ObjectType, Property, UTCDateTime}, + prelude::{Object, ObjectInner, ObjectType, Property, UTCDateTime}, structs::{Application, OAuthClient, SystemSettings}, }, types::map::Map, @@ -40,9 +40,12 @@ use store::registry::{ }; /// The client id the upstream web interface uses when its application names none. -pub const WEB_INTERFACE_CLIENT_ID: &str = "stalwart-webui"; +pub const WEB_INTERFACE_CLIENT_ID: &str = "inbuxa-webui"; pub const ADMIN_CLIENT_ID: &str = "inbuxa-admin"; pub const WEBMAIL_CLIENT_ID: &str = "ihasmail-inbuxa"; +/// The web interface's client id before the fork renamed it (SPEC §2.4). +/// Only ever read to retire it. +const LEGACY_WEB_INTERFACE_CLIENT_ID: &str = "stalwart-webui"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct FirstPartyClient { @@ -187,6 +190,7 @@ fn env(name: &str) -> Option { } pub(crate) async fn ensure_first_party_clients(bp: &mut Bootstrap) -> trc::Result<()> { + retire_legacy_web_interface_client(bp).await?; let system = bp.setting_infallible::().await; let base_url = base_url(bp, &system); let applications = bp @@ -213,6 +217,56 @@ pub(crate) async fn ensure_first_party_clients(bp: &mut Bootstrap) -> trc::Resul Ok(()) } +/// An install from before the rename, upstream's or this fork's, has the web +/// interface registered as `stalwart-webui`, and may +/// have an application naming it. The application is moved to the current id +/// and the old client removed, so the old id stops working rather than +/// living on as an alias; anyone signed in to the web interface signs in +/// again. Runs on every start and does nothing once both are gone. +async fn retire_legacy_web_interface_client(bp: &mut Bootstrap) -> trc::Result<()> { + for app in bp.list_infallible::().await { + if app.object.oauth_client_id.as_deref() != Some(LEGACY_WEB_INTERFACE_CLIENT_ID) { + continue; + } + let mut updated = app.object.clone(); + updated.oauth_client_id = Some(WEB_INTERFACE_CLIENT_ID.to_string()); + // The old object carries its revision: the write asserts on it. + let current = Object::with_revision(ObjectInner::from(app.object), app.revision); + let result = bp + .registry + .write(RegistryWrite::update(app.id.id(), &updated.into(), ¤t)) + .await?; + if !matches!(result, RegistryWriteResult::Success(_)) { + return Err(trc::StoreEvent::UnexpectedError + .into_err() + .details("Failed to move an application to the renamed web interface client.") + .reason(result.to_string()) + .caused_by(trc::location!())); + } + } + + if let Some(object_id) = bp + .registry + .primary_key( + ObjectType::OAuthClient.into(), + Property::ClientId, + LEGACY_WEB_INTERFACE_CLIENT_ID.as_bytes().to_vec(), + ) + .await? + { + let result = bp.registry.write(RegistryWrite::delete(object_id)).await?; + if !matches!(result, RegistryWriteResult::Success(_)) { + return Err(trc::StoreEvent::UnexpectedError + .into_err() + .details("Failed to remove the web interface's pre-rename OAuth client.") + .reason(result.to_string()) + .caused_by(trc::location!())); + } + } + + Ok(()) +} + async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Result<()> { let existing = match bp .registry @@ -223,15 +277,18 @@ async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Res ) .await? { + // inbuxa: read as an Object, keeping the revision the update below + // asserts on (a bare OAuthClient converts back with revision 0, which + // never matches, so any update failed start-up). Some(object_id) => bp .registry - .object::(object_id.id()) + .get(object_id) .await? - .map(|object| (object_id.id(), object)), + .map(|object| (object_id.id(), object.revision, OAuthClient::from(object))), None => None, }; - let result = if let Some((id, current)) = existing { + let result = if let Some((id, revision, current)) = existing { let mut updated = current.clone(); for uri in &client.redirect_uris { if !updated.redirect_uris.contains(uri) { @@ -255,8 +312,9 @@ async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Res if updated == current { return Ok(()); } + let current = Object::with_revision(ObjectInner::from(current), revision); bp.registry - .write(RegistryWrite::update(id, &updated.into(), ¤t.into())) + .write(RegistryWrite::update(id, &updated.into(), ¤t)) .await? } else { let secret = match &client.secret { diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs index 9ebb323..f2a30df 100644 --- a/crates/common/src/manager/mod.rs +++ b/crates/common/src/manager/mod.rs @@ -23,8 +23,8 @@ pub mod defaults; pub mod first_party; pub mod restore; -pub const SPAM_TRAINER_KEY: &[u8] = "STALWART_SPAM_TRAIN_DATA.lz4".as_bytes(); -pub const SPAM_CLASSIFIER_KEY: &[u8] = "STALWART_SPAM_CLASSIFIER_MODEL.lz4".as_bytes(); +pub const SPAM_TRAINER_KEY: &[u8] = "INBUXA_SPAM_TRAIN_DATA.lz4".as_bytes(); +pub const SPAM_CLASSIFIER_KEY: &[u8] = "INBUXA_SPAM_CLASSIFIER_MODEL.lz4".as_bytes(); pub async fn fetch_resource( url: &str, diff --git a/crates/common/src/network/webpush.rs b/crates/common/src/network/webpush.rs index abf49cc..1f8bf65 100644 --- a/crates/common/src/network/webpush.rs +++ b/crates/common/src/network/webpush.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; @@ -313,16 +315,16 @@ B4yDfR2rGOd2H6Kv3fQNHPj9Nu5Tks8QYMLzrX8ONCNoFnNUQl9S0r0QS6phVqD0 #[test] fn contact_is_normalized_to_a_uri() { for (input, expected) in [ - ("hello@stalw.art", Some("mailto:hello@stalw.art")), - (" hello@stalw.art ", Some("mailto:hello@stalw.art")), - ("mailto:hello@stalw.art", Some("mailto:hello@stalw.art")), - ("MAILTO:hello@stalw.art", Some("MAILTO:hello@stalw.art")), + ("hello@example.org", Some("mailto:hello@example.org")), + (" hello@example.org ", Some("mailto:hello@example.org")), + ("mailto:hello@example.org", Some("mailto:hello@example.org")), + ("MAILTO:hello@example.org", Some("MAILTO:hello@example.org")), ( - "https://stalw.art/contact", - Some("https://stalw.art/contact"), + "https://example.org/contact", + Some("https://example.org/contact"), ), - ("stalw.art", None), - ("http://stalw.art", None), + ("example.org", None), + ("http://example.org", None), ("tel:+123456789", None), ("", None), ] { diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index ca9f89a..f768ea4 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use super::ETag; @@ -490,7 +492,7 @@ impl LockRequestHandler for Server { for cond in &if_.list { match cond { Condition::StateToken { token, .. } => { - if token.starts_with("urn:stalwart:davsync:") { + if token.starts_with("urn:inbuxa:davsync:") { needs_sync_token = true; } else { needs_lock_token = true; diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index d1c2f98..d3040ac 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{DavError, DavResourceName}; @@ -181,12 +183,12 @@ impl OwnedUri<'_> { impl Urn { pub fn try_extract_sync_id(token: &str) -> Option<&str> { token - .strip_prefix("urn:stalwart:davsync:") + .strip_prefix("urn:inbuxa:davsync:") .map(|x| x.split_once(':').map(|(x, _)| x).unwrap_or(x)) } pub fn parse(input: &str) -> Option { - let inbox = input.strip_prefix("urn:stalwart:")?; + let inbox = input.strip_prefix("urn:inbuxa:")?; let (kind, id) = inbox.split_once(':')?; match kind { "davlock" => u64::from_str_radix(id, 16).ok().map(Urn::Lock), @@ -223,12 +225,12 @@ impl Urn { impl Display for Urn { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Urn::Lock(id) => write!(f, "urn:stalwart:davlock:{id:x}",), + Urn::Lock(id) => write!(f, "urn:inbuxa:davlock:{id:x}",), Urn::Sync { id, seq } => { if *seq == 0 { - write!(f, "urn:stalwart:davsync:{id:x}") + write!(f, "urn:inbuxa:davsync:{id:x}") } else { - write!(f, "urn:stalwart:davsync:{id:x}:{seq:x}") + write!(f, "urn:inbuxa:davsync:{id:x}:{seq:x}") } } } diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index d9779d8..8d6dcea 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -91,7 +91,7 @@ pub enum Capability { FileNode = 1 << 15, #[serde(rename(serialize = "urn:ietf:params:jmap:mail:share"))] MailShare = 1 << 16, - #[serde(rename(serialize = "urn:stalwart:jmap"))] + #[serde(rename(serialize = "urn:inbuxa:jmap:registry"))] Stalwart = 1 << 17, #[serde(rename(serialize = "urn:ietf:params:jmap:webpush-vapid"))] WebPushVapid = 1 << 18, @@ -353,7 +353,7 @@ impl Capability { Capability::PrincipalsAvailability => "urn:ietf:params:jmap:principals:availability", Capability::FileNode => "urn:ietf:params:jmap:filenode", Capability::MailShare => "urn:ietf:params:jmap:mail:share", - Capability::Stalwart => "urn:stalwart:jmap", + Capability::Stalwart => "urn:inbuxa:jmap:registry", Capability::WebPushVapid => "urn:ietf:params:jmap:webpush-vapid", Capability::EmailPush => "urn:ietf:params:jmap:emailpush", Capability::Inbuxa => "urn:inbuxa:jmap", @@ -501,7 +501,7 @@ impl Capability { "urn:ietf:params:jmap:contacts:parse" => Capability::ContactsParse, "urn:ietf:params:jmap:calendars:parse" => Capability::CalendarsParse, "urn:ietf:params:jmap:mail:share" => Capability::MailShare, - "urn:stalwart:jmap" => Capability::Stalwart, + "urn:inbuxa:jmap:registry" => Capability::Stalwart, "urn:ietf:params:jmap:webpush-vapid" => Capability::WebPushVapid, "urn:ietf:params:jmap:emailpush" => Capability::EmailPush, "urn:inbuxa:jmap" => Capability::Inbuxa, diff --git a/crates/jmap/src/registry/mapping/bootstrap.rs b/crates/jmap/src/registry/mapping/bootstrap.rs index 65a7d34..5e305ff 100644 --- a/crates/jmap/src/registry/mapping/bootstrap.rs +++ b/crates/jmap/src/registry/mapping/bootstrap.rs @@ -208,7 +208,7 @@ pub(crate) async fn bootstrap_set( .with_description(concat!( "The selected data store contains information from an older version. ", "Please follow the upgrade instructions at ", - "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" + "https://docs.inbuxa.org/install/migrating/" )), ); break; @@ -679,7 +679,7 @@ fn build_default_bootstrap(server: &Server) -> Bootstrap { directory: DirectoryBootstrap::Internal, tracer: Tracer::Log(TracerLog { path: "/var/log/inbuxa/".to_string(), - prefix: "stalwart".to_string(), + prefix: "inbuxa".to_string(), ansi: true, enable: true, ..Default::default() diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 3066afb..16cf17b 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ #![warn(clippy::large_futures)] @@ -19,6 +21,10 @@ pub mod destroy; pub mod v016; pub async fn try_migrate(server: &Server) -> trc::Result<()> { + // inbuxa: before the version check, which returns early on a current + // store, and before migrate_v0_16, which reads the renamed key. + rename_spam_blobs(server).await?; + match server .store() .get_value::(AnyKey { @@ -36,14 +42,14 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> { Some(0..=4) => { abort(concat!( "You must first upgrade to version 0.15, please read ", - "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" + "https://docs.inbuxa.org/install/migrating/" )); } Some(5) => { if !server.registry().is_recovery_mode() { abort(concat!( "Upgrading to version 0.16 is a multi-step process, please read ", - "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" + "https://docs.inbuxa.org/install/migrating/" )); } } @@ -61,7 +67,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> { } else { abort(concat!( "You must first upgrade to version 0.15, please read ", - "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" + "https://docs.inbuxa.org/install/migrating/" )); } } @@ -134,3 +140,47 @@ async fn is_new_install(server: &Server) -> trc::Result { Ok(true) } + +/// inbuxa: the spam filter's trainer and model blobs, under the names they +/// had before the fork renamed them (SPEC §2.4), paired with the current ones. +const RENAMED_SPAM_BLOBS: [(&[u8], &[u8]); 2] = [ + (b"STALWART_SPAM_TRAIN_DATA.lz4", common::manager::SPAM_TRAINER_KEY), + ( + b"STALWART_SPAM_CLASSIFIER_MODEL.lz4", + common::manager::SPAM_CLASSIFIER_KEY, + ), +]; + +/// Moves each spam blob from its pre-rename key to the current one, so a +/// trained model survives the rename. A blob already under the current key +/// wins and the old one is just removed; with neither, nothing happens. +async fn rename_spam_blobs(server: &Server) -> trc::Result<()> { + let blobs = server.blob_store(); + for (old, new) in RENAMED_SPAM_BLOBS { + let Some(data) = blobs + .get_blob(old, 0..usize::MAX) + .await + .caused_by(trc::location!())? + else { + continue; + }; + if blobs + .get_blob(new, 0..usize::MAX) + .await + .caused_by(trc::location!())? + .is_none() + { + blobs + .put_blob(new, &data, server.core.email.compression) + .await + .caused_by(trc::location!())?; + } + blobs.delete_blob(old).await.caused_by(trc::location!())?; + trc::event!( + Server(trc::ServerEvent::Startup), + Details = "Moved a spam filter blob to its renamed key", + Key = new, + ); + } + Ok(()) +} diff --git a/crates/registry/src/schema/enums_impl.rs b/crates/registry/src/schema/enums_impl.rs index bcfa372..4a8d646 100644 --- a/crates/registry/src/schema/enums_impl.rs +++ b/crates/registry/src/schema/enums_impl.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ // This file is auto-generated. Do not edit directly. @@ -10372,8 +10374,8 @@ impl EnumImpl for SieveCapability { b"spamtest" => SieveCapability::Spamtest, b"spamtestplus" => SieveCapability::Spamtestplus, b"virustest" => SieveCapability::Virustest, - b"vnd.stalwart.while" => SieveCapability::VndStalwartWhile, - b"vnd.stalwart.expressions" => SieveCapability::VndStalwartExpressions, + b"vnd.inbuxa.while" => SieveCapability::VndStalwartWhile, + b"vnd.inbuxa.expressions" => SieveCapability::VndStalwartExpressions, } } @@ -10426,8 +10428,8 @@ impl EnumImpl for SieveCapability { SieveCapability::Spamtest => "spamtest", SieveCapability::Spamtestplus => "spamtestplus", SieveCapability::Virustest => "virustest", - SieveCapability::VndStalwartWhile => "vnd.stalwart.while", - SieveCapability::VndStalwartExpressions => "vnd.stalwart.expressions", + SieveCapability::VndStalwartWhile => "vnd.inbuxa.while", + SieveCapability::VndStalwartExpressions => "vnd.inbuxa.expressions", } } diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 5a75309..270f7f3 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -29622,8 +29622,8 @@ impl Default for MySqlSettings { Self { host: Default::default(), port: 3306u64, - database: "stalwart".to_string(), - auth_username: Some("stalwart".to_string()), + database: "inbuxa".to_string(), + auth_username: Some("inbuxa".to_string()), auth_secret: Default::default(), } } @@ -29780,8 +29780,8 @@ impl Default for MySqlStore { read_replicas: Default::default(), host: Default::default(), port: 3306u64, - database: "stalwart".to_string(), - auth_username: Some("stalwart".to_string()), + database: "inbuxa".to_string(), + auth_username: Some("inbuxa".to_string()), auth_secret: Default::default(), } } @@ -29942,7 +29942,7 @@ impl Default for NatsCoordinator { no_echo: true, use_tls: false, auth_secret: Default::default(), - auth_username: Some("stalwart".to_string()), + auth_username: Some("inbuxa".to_string()), credentials: Default::default(), } } @@ -31125,8 +31125,8 @@ impl Default for PostgreSqlSettings { Self { host: Default::default(), port: 5432u64, - database: "stalwart".to_string(), - auth_username: Some("stalwart".to_string()), + database: "inbuxa".to_string(), + auth_username: Some("inbuxa".to_string()), auth_secret: Default::default(), options: Default::default(), } @@ -31270,8 +31270,8 @@ impl Default for PostgreSqlStore { read_replicas: Default::default(), host: Default::default(), port: 5432u64, - database: "stalwart".to_string(), - auth_username: Some("stalwart".to_string()), + database: "inbuxa".to_string(), + auth_username: Some("inbuxa".to_string()), auth_secret: Default::default(), options: Default::default(), } @@ -32576,7 +32576,7 @@ impl Default for RedisClusterStore { Self { urls: Map::new(vec!["redis://127.0.0.1".to_string()]), timeout: Duration::from_millis(10000), - auth_username: Some("stalwart".to_string()), + auth_username: Some("inbuxa".to_string()), auth_secret: Default::default(), max_retry_wait: Default::default(), min_retry_wait: Default::default(), @@ -32743,7 +32743,7 @@ impl Default for RedisSentinelStore { urls: Map::new(vec!["redis://127.0.0.1:26379".to_string()]), service_name: "mymaster".to_string(), timeout: Duration::from_millis(10000), - auth_username: Some("stalwart".to_string()), + auth_username: Some("inbuxa".to_string()), auth_secret: Default::default(), sentinel_username: Default::default(), sentinel_secret: Default::default(), @@ -46308,7 +46308,7 @@ impl Default for TracerLog { fn default() -> Self { Self { path: Default::default(), - prefix: "stalwart".to_string(), + prefix: "inbuxa".to_string(), rotate: LogRotateFrequency::Daily, ansi: true, multiline: false, diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index bd19341..ee0ba14 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -1,9 +1,8 @@ [package] name = "smtp" -description = "Stalwart SMTP Server" +description = "inbuxa SMTP server" authors = [ "Stalwart Labs LLC "] -repository = "https://github.com/stalwartlabs/smtp-server" -homepage = "https://stalw.art/smtp" +homepage = "https://inbuxa.org" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" diff --git a/crates/store/src/build/registry.rs b/crates/store/src/build/registry.rs index 8b352c9..df6bded 100644 --- a/crates/store/src/build/registry.rs +++ b/crates/store/src/build/registry.rs @@ -27,7 +27,7 @@ impl RegistryStore { // variable, so it's ignored there, and loudly. if !inner.env_recovery_mode && inner.env_recovery_admin.take().is_some() { eprintln!(); - eprintln!("⚠️ INBUXA_RECOVERY_ADMIN (or STALWART_RECOVERY_ADMIN) is set, but the"); + eprintln!("⚠️ INBUXA_RECOVERY_ADMIN is set, but the"); eprintln!(" server is configured and not in recovery mode, so it is ignored."); eprintln!(" Remove it from the environment. To use it for recovery, also set"); eprintln!(" INBUXA_RECOVERY_MODE=1."); diff --git a/crates/types/src/branding.rs b/crates/types/src/branding.rs index 5e471c9..d1f5fb1 100644 --- a/crates/types/src/branding.rs +++ b/crates/types/src/branding.rs @@ -47,22 +47,32 @@ macro_rules! brand_url { } /// Reads one of the server's environment variables by its unprefixed name, -/// such as `RECOVERY_ADMIN`. +/// such as `RECOVERY_ADMIN`, from `INBUXA_`. /// -/// `INBUXA_` wins. `STALWART_` is still read when the new name -/// isn't set, so an existing Stalwart install moves over without editing its -/// environment, and a warning says which variable to rename. +/// The upstream prefix isn't read (SPEC §2.4). An install moved over from +/// upstream that still sets it stops here with the variable to rename, rather +/// than starting on defaults the operator didn't choose. pub fn env_var(name: &str) -> Result { - match std::env::var(format!("INBUXA_{name}")) { - Err(std::env::VarError::NotPresent) => { - let legacy = std::env::var(format!("STALWART_{name}")); - if legacy.is_ok() { - eprintln!("Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead."); - } - legacy - } - found => found, + let found = std::env::var(format!("INBUXA_{name}")); + if matches!(found, Err(std::env::VarError::NotPresent)) + && let Some(legacy) = legacy_setting(name, |var| std::env::var_os(var).is_some()) + { + eprintln!( + "Error: {legacy} is set, but inbuxa reads INBUXA_{name}. Rename it and start again \ + (https://docs.inbuxa.org/install/migrating/)." + ); + std::process::exit(1); } + found +} + +/// The environment prefix upstream reads. Only ever used to refuse it. +const LEGACY_ENV_PREFIX: &str = "STALWART"; + +/// The upstream-prefixed variable for `name`, if it's set. +fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option { + let legacy = format!("{LEGACY_ENV_PREFIX}_{name}"); + is_set(&legacy).then_some(legacy) } /// INBUXA's own version, dated like the rest of its family: `YYYY.M.D`, with @@ -90,3 +100,16 @@ macro_rules! brand_version_full { concat!($crate::brand_version!(), " (upstream ", env!("CARGO_PKG_VERSION"), ")") }; } + +#[cfg(test)] +mod tests { + use super::{LEGACY_ENV_PREFIX, legacy_setting}; + + #[test] + fn an_upstream_setting_is_named_for_renaming() { + let old = format!("{LEGACY_ENV_PREFIX}_RECOVERY_ADMIN"); + let set = |var: &str| var == old; + assert_eq!(legacy_setting("RECOVERY_ADMIN", set), Some(old.clone())); + assert_eq!(legacy_setting("HOSTNAME", set), None); + } +} diff --git a/docs/spec/SPEC.md b/docs/spec/SPEC.md index cddc043..dffbf7a 100644 --- a/docs/spec/SPEC.md +++ b/docs/spec/SPEC.md @@ -212,14 +212,29 @@ depends on `store` and can't be called from it (`features/scale-out-storage.md`) - Factual statements are allowed and required: "a fork of Stalwart", "compatible with Stalwart 0.16 data". Upstream copyright notices stay on every file they cover. -- Protocol identifiers stay as upstream has them, for example the JMAP - capability `urn:stalwart:jmap` and the `x:` object names. They're - interoperability, not branding, and renaming them breaks every existing - client. Anything the fork adds uses its own namespace (open: which one). -- **Version and build metadata are exempt from the first bullet** (added - 2026-09-19). `inbuxa --version`, the startup banner and events, - OpenTelemetry's `service.version`, the JMAP `implementation` string, release - notes and the strip report all name the Stalwart base, as §2.6 requires. +- **Identifiers people meet carry the fork's name** (changed 2026-09-22; + this bullet used to keep upstream's). Upstream's JMAP capability for the + registry (`x:`) objects is `urn:inbuxa:jmap:registry`, beside the fork's + own `urn:inbuxa:jmap` (contract C-1); WebDAV lock and sync tokens are + `urn:inbuxa:dav*`, the Sieve extensions are `vnd.inbuxa.while` and `vnd.inbuxa.expressions`, the + web interface's OAuth client is `inbuxa-webui`, the spam filter's blobs are + `INBUXA_SPAM_*`, and the SQL stores and log files default to `inbuxa`. + There are no aliases: the old names stop working, so front ends move with + the server, Sieve scripts that `require` the old extensions are edited, + DAV clients resync once, and anyone signed in to the web interface signs in + again. Pre-rename data is carried over where it would otherwise be lost + (the spam model, the web interface's client). The `x:` object names are + unchanged, having no name in them. `tools/fork/renames.py` holds the list, + and the strip applies it to every import (§2.2). +- **Identifiers nobody sees keep upstream's spelling:** the OAuth + key-derivation contexts, whose renaming would invalidate every issued token + and client id, and the application blob prefix, which is hashed before use. + `tools/fork/name-allowlist.txt` lists them with their reasons. +- **Version and build metadata give the base without the name** (added + 2026-09-19, narrowed 2026-09-22). `inbuxa --version`, the startup banner and + events, OpenTelemetry's `service.version` and the JMAP `implementation` + string say `2026.9.23 (upstream 0.16.23)`, as §2.6 requires; release notes + and the strip report may name the Stalwart base. That is a factual statement about what was compiled, not a name the product calls itself, and the two bullets don't conflict: the first governs identity, this one governs provenance. A reader who takes "no Stalwart in @@ -243,15 +258,18 @@ Done 2026-09-18: - The package and binary are `inbuxa` (`cargo build -p inbuxa`). The binary's help, banner and every protocol greeting say INBUXA (the branding module, `types::brand!()`). -- Settings come from `INBUXA_*` environment variables. Each still falls back - to its `STALWART_*` name, with a startup warning to rename it - (`types::branding::env_var`). That covers all nine the server reads: - `HOSTNAME`, `RECOVERY_MODE`, `RECOVERY_ADMIN`, `RECOVERY_MODE_PORT`, - `RECOVERY_MODE_LOG_LEVEL`, `ROLE`, `PUSH_SHARD`, `PUBLIC_URL`, `HTTPS_PORT`. -- **Not renamed, on purpose:** `STALWART_APP_` and the two `STALWART_SPAM_...` - names. They look like environment variables, but they're keys inside the - data store, so renaming them would orphan existing installed apps and - spam-classifier models. +- Settings come from `INBUXA_*` environment variables + (`types::branding::env_var`): `HOSTNAME`, `RECOVERY_MODE`, `RECOVERY_ADMIN`, + `RECOVERY_MODE_PORT`, `RECOVERY_MODE_LOG_LEVEL`, `ROLE`, `PUSH_SHARD`, + `PUBLIC_URL`, `HTTPS_PORT`, and the front-end variables of contract C-6. + Until 2026-09-22 each fell back to its `STALWART_*` name with a warning. + Now a `STALWART_*` name that's set where its `INBUXA_*` one isn't stops the + server at startup, naming the variable to rename, so an install moved over + from upstream never runs on settings it silently dropped. +- The spam filter's blobs moved from `STALWART_SPAM_*` to `INBUXA_SPAM_*` on + 2026-09-22; every start moves any left under the old keys + (`migration::try_migrate`), so no trained model is orphaned. + `STALWART_APP_` stays: it's hashed into a blob key and never seen. - New installs default to `/var/lib/inbuxa` for data and `/var/log/inbuxa` for logs. Existing installs keep the paths their configuration names, so no data moves. @@ -421,8 +439,8 @@ Versioned, and advertised in the JMAP session so either side can check it. address or network, so an admin credential is useless from anywhere else. - **Push.** Unchanged: JMAP push with VAPID, as ihasmail uses today. - **INBUXA Admin's client.** INBUXA Admin signs in by OAuth (authorization - code with PKCE) as upstream's `webui` does, as client `stalwart-webui` for - now. The fork registers a first-party `inbuxa-admin` client with the + code with PKCE) as upstream's `webui` does, as client `inbuxa-webui` + (upstream's `stalwart-webui` until 2026-09-22) when the server serves it. The fork registers a first-party `inbuxa-admin` client with the admin's own redirect URIs, and the admin switches to it (its ``). - **Cross-origin access.** Verified 2026-09-18 against a separate diff --git a/docs/spec/contract.md b/docs/spec/contract.md index b911cc6..451b46c 100644 --- a/docs/spec/contract.md +++ b/docs/spec/contract.md @@ -106,8 +106,9 @@ Each has an ID, and tests name the IDs they check. ihasmail-inbuxa server, authorization code with PKCE S256, redirect URI `{webmailUrl}/api/auth/callback`. INBUXA Admin's `` is set to `inbuxa-admin`. - Until then it keeps upstream's `stalwart-webui`, which only works while - registration isn't required. + Served by the server itself it uses the web interface's client, + `inbuxa-webui` (upstream's `stalwart-webui` until 2026-09-22, retired on + start since). **Built (interim), 2026-09-18.** C-5's defaults are in the server, and `crates/common/src/manager/first_party.rs` registers the clients on every @@ -116,7 +117,7 @@ Each has an ID, and tests name the IDs they check. `INBUXA_WEBMAIL_CLIENT_SECRET` (the webmail client is registered only when both of its variables are set). A web interface the server serves itself (none on a new install since SPEC.md §5.3; possible on one upgraded from - Stalwart) is registered too, as its application's OAuth client id or `stalwart-webui`, at the + Stalwart) is registered too, as its application's OAuth client id or `inbuxa-webui`, at the server's public URL. A missing client is created. An existing one gains any redirect URI it lacks, and the webmail client gets the configured secret. Nothing an operator added is removed. Bootstrap and recovery mode skip this: diff --git a/docs/spec/features/ai-spam-classification.md b/docs/spec/features/ai-spam-classification.md index 6153939..52621f7 100644 --- a/docs/spec/features/ai-spam-classification.md +++ b/docs/spec/features/ai-spam-classification.md @@ -291,7 +291,7 @@ adds at most 2. ### The Sieve function `llm_prompt` - **AI-20.** `llm_prompt(model, prompt, temperature)`, available with - `require "vnd.stalwart.expressions"`. `model` names an `x:AiModel` by its + `require "vnd.inbuxa.expressions"`. `model` names an `x:AiModel` by its `name`, or failing that by its id. `prompt` is sent as is. `temperature` is clamped to 0.0–1.0. A value that isn't a number uses the model's own `temperature`. **Decision** on name first, see open question 4. diff --git a/docs/spec/features/masked-email.md b/docs/spec/features/masked-email.md index 1a5a0cc..89a4d89 100644 --- a/docs/spec/features/masked-email.md +++ b/docs/spec/features/masked-email.md @@ -210,7 +210,7 @@ accepted, which is the fact `enabled` reports. ### Upstream's, unchanged -`x:MaskedEmail/get`, `/query`, `/set` under `urn:stalwart:jmap`, standard RFC +`x:MaskedEmail/get`, `/query`, `/set` under `urn:inbuxa:jmap:registry`, standard RFC 8620 shapes, the record above. Upstream's `/query` accepts only an `accountId` filter, and it has no `/changes` (observed 6). diff --git a/docs/spec/features/monitoring.md b/docs/spec/features/monitoring.md index 704cb0f..65ace3e 100644 --- a/docs/spec/features/monitoring.md +++ b/docs/spec/features/monitoring.md @@ -374,7 +374,7 @@ unchanged. ## Interfaces -- **JMAP, existing names, unchanged.** Over `urn:stalwart:jmap`: +- **JMAP, existing names, unchanged.** Over `urn:inbuxa:jmap:registry`: - `x:Alert/get`, `/query`, `/set`, and the `x:TracingStore`, `x:MetricsStore`, `x:DataRetention`, `x:Search` singletons. - `x:Trace/get` and `/query`. Filters: `text`, `timestamp` (comparison names diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index 3806d5d..f949942 100644 Binary files a/resources/schema/schema.json.gz and b/resources/schema/schema.json.gz differ diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 480c5d9..dad5cc1 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -rLRbZKj15KvcPMVmnisfCDsXXZEKks6BXpMkMpS0mlI \ No newline at end of file +wa2yYpZjVWiT3bW8anintWsnNSjDQUXa8U-Va-s6mZQ \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml index cbcdb55..f561122 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -49,6 +49,7 @@ email = { path = "../crates/email", features = ["test_mode"] } spam-filter = { path = "../crates/spam-filter", features = ["test_mode"] } trc = { path = "../crates/trc", features = [] } managesieve = { path = "../crates/managesieve", features = ["test_mode"] } +migration = { path = "../crates/migration" } smtp-proto = { version = "0.2" } mail-auth = { version = "0.13", features = ["test"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } diff --git a/tests/e2e/legacy_protocols.py b/tests/e2e/legacy_protocols.py index a27706f..c011849 100755 --- a/tests/e2e/legacy_protocols.py +++ b/tests/e2e/legacy_protocols.py @@ -105,7 +105,7 @@ def stop(): docker("rm", "-f", NAME, check_rc=False) -def jmap(user, password, calls, using=("urn:ietf:params:jmap:core", "urn:stalwart:jmap", INBUXA)): +def jmap(user, password, calls, using=("urn:ietf:params:jmap:core", "urn:inbuxa:jmap:registry", INBUXA)): body = json.dumps({"using": list(using), "methodCalls": calls}).encode() req = urllib.request.Request(f"{HTTP}/jmap/", data=body, method="POST") req.add_header("Content-Type", "application/json") diff --git a/tests/resources/jmap/sieve/test_mailbox.sieve b/tests/resources/jmap/sieve/test_mailbox.sieve index c3e38a5..d75ef47 100644 --- a/tests/resources/jmap/sieve/test_mailbox.sieve +++ b/tests/resources/jmap/sieve/test_mailbox.sieve @@ -1,4 +1,4 @@ -require ["fileinto", "mailbox", "mailboxid", "special-use", "ihave", "imap4flags", "vnd.stalwart.expressions"]; +require ["fileinto", "mailbox", "mailboxid", "special-use", "ihave", "imap4flags", "vnd.inbuxa.expressions"]; # SpecialUse extension tests if not specialuse_exists ["inbox", "trash"] { diff --git a/tests/resources/scripts/stress_test_prepare.py b/tests/resources/scripts/stress_test_prepare.py index 426a5ee..456d8e0 100644 --- a/tests/resources/scripts/stress_test_prepare.py +++ b/tests/resources/scripts/stress_test_prepare.py @@ -8,7 +8,7 @@ import urllib.error import urllib.request CORE = "urn:ietf:params:jmap:core" -STALWART = "urn:stalwart:jmap" +STALWART = "urn:inbuxa:jmap:registry" USING = [CORE, STALWART] DEFAULT_BASE_URL = "https://127.0.0.1" diff --git a/tests/resources/smtp/sieve/awl.sieve b/tests/resources/smtp/sieve/awl.sieve index d3ab078..9904664 100644 --- a/tests/resources/smtp/sieve/awl.sieve +++ b/tests/resources/smtp/sieve/awl.sieve @@ -1,4 +1,4 @@ -require ["variables", "include", "vnd.stalwart.expressions", "reject"]; +require ["variables", "include", "vnd.inbuxa.expressions", "reject"]; global "score"; diff --git a/tests/resources/smtp/sieve/awl_include.sieve b/tests/resources/smtp/sieve/awl_include.sieve index 64ff476..de5af60 100644 --- a/tests/resources/smtp/sieve/awl_include.sieve +++ b/tests/resources/smtp/sieve/awl_include.sieve @@ -1,4 +1,4 @@ -require ["variables", "include", "vnd.stalwart.expressions", "reject"]; +require ["variables", "include", "vnd.inbuxa.expressions", "reject"]; global "score"; set "awl_factor" "0.5"; diff --git a/tests/resources/smtp/sieve/stage_mail.sieve b/tests/resources/smtp/sieve/stage_mail.sieve index 7c67b38..a1e8bc2 100644 --- a/tests/resources/smtp/sieve/stage_mail.sieve +++ b/tests/resources/smtp/sieve/stage_mail.sieve @@ -1,4 +1,4 @@ -require ["variables", "envelope", "reject", "vnd.stalwart.expressions"]; +require ["variables", "envelope", "reject", "vnd.inbuxa.expressions"]; if envelope :localpart :is "from" "spammer" { reject "450 4.1.1 Invalid address"; diff --git a/tests/resources/smtp/sieve/stage_rcpt.sieve b/tests/resources/smtp/sieve/stage_rcpt.sieve index c95d441..aa91a8f 100644 --- a/tests/resources/smtp/sieve/stage_rcpt.sieve +++ b/tests/resources/smtp/sieve/stage_rcpt.sieve @@ -1,4 +1,4 @@ -require ["variables", "envelope", "reject", "vnd.stalwart.expressions"]; +require ["variables", "envelope", "reject", "vnd.inbuxa.expressions"]; if envelope :domain :is "to" "foobar.org" { eval "query('sql', 'CREATE TABLE IF NOT EXISTS greylist (addr TEXT PRIMARY KEY)', [])"; diff --git a/tests/src/jmap/principal/get.rs b/tests/src/jmap/principal/get.rs index f69c707..e1176a6 100644 --- a/tests/src/jmap/principal/get.rs +++ b/tests/src/jmap/principal/get.rs @@ -250,9 +250,10 @@ pub async fn test(test: &TestServer) { "webWriteUrlTemplate": null }, "urn:ietf:params:jmap:mail:share": {}, - "urn:stalwart:jmap": {}, - // inbuxa: MT-22, the logo that applies to the account - "urn:inbuxa:jmap": { "logo": null }, + "urn:inbuxa:jmap:registry": {}, + // inbuxa: MT-22, the logo that applies to the account, and + // LP-19, whether the legacy protocols are open to it + "urn:inbuxa:jmap": { "logo": null, "legacyProtocols": "enabled" }, "https://www.fastmail.com/dev/maskedemail": {} } } @@ -274,7 +275,7 @@ pub async fn test(test: &TestServer) { "urn:ietf:params:jmap:principals:availability": john_id, "urn:ietf:params:jmap:filenode": john_id, "urn:ietf:params:jmap:mail:share": john_id, - "urn:stalwart:jmap": john_id, + "urn:inbuxa:jmap:registry": john_id, "https://www.fastmail.com/dev/maskedemail": john_id }, "username": "jdoe@example.com", diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 93b5f56..4f1abc0 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -30,6 +30,8 @@ pub mod imap; #[cfg(test)] pub mod jmap; #[cfg(test)] +pub mod renamed_identifiers; // inbuxa: SPEC.md §2.4 +#[cfg(test)] pub mod scim; #[cfg(test)] pub mod smtp; diff --git a/tests/src/renamed_identifiers.rs b/tests/src/renamed_identifiers.rs new file mode 100644 index 0000000..ee8300b --- /dev/null +++ b/tests/src/renamed_identifiers.rs @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//! inbuxa: what an install from before the rename carries over (SPEC.md +//! §2.4). The web interface's OAuth client is retired rather than kept as an +//! alias, and a trained spam filter moves to its new keys. + +use crate::utils::server::TestServerBuilder; +use common::manager::{SPAM_CLASSIFIER_KEY, SPAM_TRAINER_KEY, first_party::WEB_INTERFACE_CLIENT_ID}; +use registry::{ + schema::{ + enums::CompressionAlgo, + prelude::{ObjectType, Property}, + structs::{Application, OAuthClient}, + }, + types::map::Map, +}; + +const OLD_CLIENT_ID: &str = "stalwart-webui"; +const OLD_TRAINER_KEY: &[u8] = b"STALWART_SPAM_TRAIN_DATA.lz4"; +const OLD_CLASSIFIER_KEY: &[u8] = b"STALWART_SPAM_CLASSIFIER_MODEL.lz4"; + +#[tokio::test(flavor = "multi_thread")] +async fn renamed_identifiers() { + // The web interface registered under upstream's client id, and an + // application naming it. Disabled, so nothing is fetched for it. + let test = TestServerBuilder::new("renamed_identifiers") + .await + .with_object(OAuthClient { + client_id: OLD_CLIENT_ID.to_string(), + redirect_uris: Map::new(vec!["https://127.0.0.1/admin/oauth/callback".to_string()]), + ..Default::default() + }) + .await + .with_object(Application { + enabled: false, + description: "Web interface".to_string(), + resource_url: "https://127.0.0.1/webui.zip".to_string(), + url_prefix: Map::new(vec!["/admin".to_string()]), + oauth_client_id: Some(OLD_CLIENT_ID.to_string()), + ..Default::default() + }) + .await + .disable_services() + .build() + .await; + + println!("Running renamed identifier tests..."); + let registry = test.server.registry(); + assert_eq!( + registry + .primary_key( + ObjectType::OAuthClient.into(), + Property::ClientId, + OLD_CLIENT_ID.as_bytes().to_vec(), + ) + .await + .unwrap(), + None, + "the pre-rename web interface client is retired on start" + ); + let applications = registry.list::().await.unwrap(); + assert_eq!(applications.len(), 1); + assert_eq!( + applications[0].object.oauth_client_id.as_deref(), + Some(WEB_INTERFACE_CLIENT_ID), + "an application naming the old client moves to the new one" + ); + + // A trained model under the pre-rename keys moves to the new ones. + let blobs = test.server.blob_store(); + for (key, data) in [ + (OLD_TRAINER_KEY, &b"trainer"[..]), + (OLD_CLASSIFIER_KEY, &b"classifier"[..]), + ] { + blobs.put_blob(key, data, CompressionAlgo::None).await.unwrap(); + } + migration::try_migrate(&test.server).await.unwrap(); + for (old, new, data) in [ + (OLD_TRAINER_KEY, SPAM_TRAINER_KEY, &b"trainer"[..]), + (OLD_CLASSIFIER_KEY, SPAM_CLASSIFIER_KEY, &b"classifier"[..]), + ] { + assert_eq!(blobs.get_blob(new, 0..usize::MAX).await.unwrap().as_deref(), Some(data)); + assert_eq!(blobs.get_blob(old, 0..usize::MAX).await.unwrap(), None); + } + + // A blob already under the new key wins over a stale old one. + blobs + .put_blob(OLD_CLASSIFIER_KEY, b"stale", CompressionAlgo::None) + .await + .unwrap(); + migration::try_migrate(&test.server).await.unwrap(); + assert_eq!( + blobs + .get_blob(SPAM_CLASSIFIER_KEY, 0..usize::MAX) + .await + .unwrap() + .as_deref(), + Some(&b"classifier"[..]) + ); + assert_eq!(blobs.get_blob(OLD_CLASSIFIER_KEY, 0..usize::MAX).await.unwrap(), None); + + // With nothing left to move, starting again changes nothing. + migration::try_migrate(&test.server).await.unwrap(); + assert_eq!( + blobs.get_blob(SPAM_TRAINER_KEY, 0..usize::MAX).await.unwrap().as_deref(), + Some(&b"trainer"[..]) + ); +} diff --git a/tests/src/system/ai.rs b/tests/src/system/ai.rs index a840c06..f7b169d 100644 --- a/tests/src/system/ai.rs +++ b/tests/src/system/ai.rs @@ -479,7 +479,7 @@ pub async fn test(test: &mut TestServer) { .await; stub.set(Mode::Echo); user.activate_script(concat!( - "require [\"vnd.stalwart.expressions\", \"editheader\", \"variables\"];\n", + "require [\"vnd.inbuxa.expressions\", \"editheader\", \"variables\"];\n", "let \"a\" \"llm_prompt('echo-test', 'hello world', 0.5)\";\n", "let \"b\" \"llm_prompt('no-such-model', 'x', 0.5)\";\n", "addheader \"X-Llm-Echo\" \"${a}\";\n", diff --git a/tests/src/utils/jmap.rs b/tests/src/utils/jmap.rs index 2b6c019..254aae0 100644 --- a/tests/src/utils/jmap.rs +++ b/tests/src/utils/jmap.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::utils::account::Account; @@ -408,7 +410,7 @@ impl Account { "urn:ietf:params:jmap:principals:availability", "urn:ietf:params:jmap:filenode", "urn:ietf:params:jmap:mail:share", - "urn:stalwart:jmap" + "urn:inbuxa:jmap:registry" ], "methodCalls": calls }); diff --git a/tests/src/webdav/lock.rs b/tests/src/webdav/lock.rs index c846966..4d729c0 100644 --- a/tests/src/webdav/lock.rs +++ b/tests/src/webdav/lock.rs @@ -2,6 +2,8 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; @@ -45,7 +47,7 @@ pub async fn test(test: &TestServer) { // Test 2: Refreshing a lock token with an invalid a lock token should fail client - .lock_refresh(&path, "urn:stalwart:davlock:1234", "infinity", "Second-456") + .lock_refresh(&path, "urn:inbuxa:davlock:1234", "infinity", "Second-456") .await .with_status(StatusCode::PRECONDITION_FAILED); @@ -148,7 +150,7 @@ pub async fn test(test: &TestServer) { // Test 10: Unlock with and without a lock token client - .unlock(&path, "urn:stalwart:davlock:1234") + .unlock(&path, "urn:inbuxa:davlock:1234") .await .with_status(StatusCode::CONFLICT) .with_value("D:error.D:lock-token-matches-request-uri", ""); diff --git a/tools/fork/name-allowlist.txt b/tools/fork/name-allowlist.txt index a279f62..8a80b6e 100644 --- a/tools/fork/name-allowlist.txt +++ b/tools/fork/name-allowlist.txt @@ -2,58 +2,27 @@ # Read by tools/fork/name-check.py. One per line: pathliteral as a JSON # string, as `name-check.py --list` prints it. Each group says why it stays; # add a line only under a reason, or with a new one. +# +# Everything clients, users and operators meet was renamed on 2026-09-22 +# (docs/spec/SPEC.md §2.4, tools/fork/renames.py). What's left is invisible, +# or names the old spelling in order to retire it. -# Key-derivation contexts. Renaming them invalidates every sealed OAuth token -# and client id already issued. +# Key-derivation contexts. Nobody sees them, and renaming them would +# invalidate every sealed OAuth token and client id already issued. crates/common/src/auth/oauth/client_id.rs "stalwart-oauth-client-id-sw1" crates/common/src/auth/oauth/token.rs "stalwart-oauth-token-sw1" -# Keys and prefixes of data already in the store. +# Hashed before it's used as a blob key, so it never appears anywhere. crates/common/src/manager/application.rs "STALWART_APP_" -crates/common/src/manager/mod.rs "STALWART_SPAM_CLASSIFIER_MODEL.lz4" -crates/common/src/manager/mod.rs "STALWART_SPAM_TRAIN_DATA.lz4" -# The web interface's OAuth client id, which existing installs and the admin -# front end already use. The id itself, and the tests that check it. +# Pre-rename names, read only to retire them: the web interface's OAuth +# client (removed on start), the spam filter's blobs (moved to their new +# keys), and the environment prefix (refused with the variable to rename). crates/common/src/manager/first_party.rs "stalwart-webui" -crates/common/src/manager/application.rs "stalwart-webui" -crates/common/src/manager/application.rs "" +crates/migration/src/lib.rs "STALWART_SPAM_CLASSIFIER_MODEL.lz4" +crates/migration/src/lib.rs "STALWART_SPAM_TRAIN_DATA.lz4" +crates/types/src/branding.rs "STALWART" -# Wire-protocol identifiers clients already hold or negotiate: WebDAV lock and -# sync tokens, the JMAP capability, Sieve extensions. -crates/dav/src/common/lock.rs "urn:stalwart:davsync:" -crates/dav/src/common/uri.rs "urn:stalwart:" -crates/dav/src/common/uri.rs "urn:stalwart:davlock:{id:x}" -crates/dav/src/common/uri.rs "urn:stalwart:davsync:" -crates/dav/src/common/uri.rs "urn:stalwart:davsync:{id:x}" -crates/dav/src/common/uri.rs "urn:stalwart:davsync:{id:x}:{seq:x}" -crates/jmap-proto/src/request/capability.rs "urn:stalwart:jmap" -crates/registry/src/schema/enums_impl.rs "vnd.stalwart.expressions" -crates/registry/src/schema/enums_impl.rs "vnd.stalwart.while" - -# Moving an existing upstream installation over: its environment variables, -# and upstream's upgrade guide for the store conversion. -crates/types/src/branding.rs "STALWART_{name}" -crates/types/src/branding.rs "Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead." -crates/store/src/build/registry.rs "⚠️ INBUXA_RECOVERY_ADMIN (or STALWART_RECOVERY_ADMIN) is set, but the" -crates/jmap/src/registry/mapping/bootstrap.rs "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" -crates/migration/src/lib.rs "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" - -# Upstream's published spam-filter rules, fetched at runtime. +# OPEN, not yet decided (2026-09-22): upstream's published spam-filter rules, +# which the server downloads at runtime from this address. crates/registry/src/schema/structs_impl.rs "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz" - -# Test fixtures: web-push contact address parsing. -crates/common/src/network/webpush.rs " hello@stalw.art " -crates/common/src/network/webpush.rs "MAILTO:hello@stalw.art" -crates/common/src/network/webpush.rs "hello@stalw.art" -crates/common/src/network/webpush.rs "http://stalw.art" -crates/common/src/network/webpush.rs "https://stalw.art/contact" -crates/common/src/network/webpush.rs "mailto:hello@stalw.art" -crates/common/src/network/webpush.rs "stalw.art" - -# OPEN, not yet decided (2026-09-22): operator-visible defaults. The log file -# prefix (TracerLog, and the bootstrap's tracer) names files stalwart.* in -# /var/log/inbuxa/, and the SQL stores default their database and user to -# "stalwart". Changing the SQL defaults would break an install relying on them. -crates/jmap/src/registry/mapping/bootstrap.rs "stalwart" -crates/registry/src/schema/structs_impl.rs "stalwart" diff --git a/tools/fork/name-check.py b/tools/fork/name-check.py index a430a7f..1ec880f 100755 --- a/tools/fork/name-check.py +++ b/tools/fork/name-check.py @@ -13,7 +13,8 @@ names, descriptions -- carries INBUXA's. Merging an upstream release brings new strings in with the name, and the merge itself can't tell, so this runs in CI on every push and pull request. -Scope: string literals in `crates/**/*.rs`, test directories excluded. +Scope: string literals in `crates/**/*.rs` and `vendor/**/*.rs`, test +directories excluded. Comments are skipped, so copyright headers and doc comments never match. Some literals have to keep the name -- key-derivation contexts, wire-protocol identifiers, defaults that read an upstream installation -- and those are @@ -79,8 +80,14 @@ def literals(src): def findings(): found = set() - crates = os.path.join(ROOT, 'crates') - for dirpath, dirnames, filenames in os.walk(crates): + for top in ('crates', 'vendor'): + found |= findings_under(os.path.join(ROOT, top)) + return found + + +def findings_under(top): + found = set() + for dirpath, dirnames, filenames in os.walk(top): dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS) for f in sorted(filenames): if not f.endswith('.rs'): diff --git a/vendor/sieve-rs/.github/workflows/rust.yml b/vendor/sieve-rs/.github/workflows/rust.yml new file mode 100644 index 0000000..5c2da0c --- /dev/null +++ b/vendor/sieve-rs/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: "CI" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/vendor/sieve-rs/.gitignore b/vendor/sieve-rs/.gitignore new file mode 100644 index 0000000..7c2f6f1 --- /dev/null +++ b/vendor/sieve-rs/.gitignore @@ -0,0 +1,12 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk +*.failed +.DS_Store diff --git a/vendor/sieve-rs/CHANGELOG.md b/vendor/sieve-rs/CHANGELOG.md new file mode 100644 index 0000000..72d43d3 --- /dev/null +++ b/vendor/sieve-rs/CHANGELOG.md @@ -0,0 +1,62 @@ +sieve-rs 0.7.3 +================================ +- Bump `mail-builder` dependency to 0.5. + +sieve-rs 0.7.2 +================================ +- Fix: `replace` action adds additional `From` header. + +sieve-rs 0.7.1 +================================ +- Bump `fancy-regex` dependency to 0.17.0. +- Fixed `rkyv` serialization lifetimes for rustc 1.93.0. + +sieve-rs 0.7.0 +================================ +- Added `rkyv` support. +- Bump `mail-parser` dependency to 0.11.0. +- Fix: Allow redirect to sender (#11). + +sieve-rs 0.6.0 +================================ +- Replaced `phf` with `hashify`. +- Bump `mail-parser` dependency to 0.10.0. + +sieve-rs 0.5.3 +================================ +- Fixed `register_match_var` function. + +sieve-rs 0.5.2 +================================ +- Fixed `set_global_variable` function. + +sieve-rs 0.5.1 +================================ +- Case insensitive envelope tests (#6). +- Set envelope variables internally (#10). + +sieve-rs 0.5.0 +================================ +- Removed context. + +sieve-rs 0.4.0 +================================ +- Support for expressions. + +sieve-rs 0.3.1 +================================ +- Bump `mail-builder` dependency to 0.3.0. + +sieve-rs 0.3.0 +================================ +- Updated ``execute`` grammar. +- Upgraded to latest mail-parser. +- Envelope accessible from environment variables. + +sieve-rs 0.2.0 +================================ +- Improved event loop. + +sieve-rs 0.1.0 +================================ +- Initial release. diff --git a/vendor/sieve-rs/Cargo.toml b/vendor/sieve-rs/Cargo.toml new file mode 100644 index 0000000..519e9ee --- /dev/null +++ b/vendor/sieve-rs/Cargo.toml @@ -0,0 +1,106 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "sieve-rs" +version = "0.7.3" +authors = ["Stalwart Labs "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Sieve filter interpreter for Rust" +homepage = "https://github.com/stalwartlabs/sieve" +readme = "README.md" +keywords = [ + "sieve", + "interpreter", + "compiler", + "email", + "mail", +] +categories = [ + "email", + "compilers", +] +license = "AGPL-3.0-only" +repository = "https://github.com/stalwartlabs/sieve" + +[features] +default = [] +rkyv = [ + "dep:rkyv", + "mail-parser/rkyv", +] +serde = [ + "dep:serde", + "mail-parser/serde", +] + +[lib] +name = "sieve" +path = "src/lib.rs" +doctest = false + +[dependencies.ahash] +version = "0.8.0" + +[dependencies.arc-swap] +version = "1.7.1" + +[dependencies.fancy-regex] +version = "0.19.0" + +[dependencies.hashify] +version = "0.2" + +[dependencies.mail-builder] +version = "0.5" + +[dependencies.mail-parser] +version = "0.11" +features = ["full_encoding"] + +[dependencies.rkyv] +version = "0.8" +optional = true + +[dependencies.serde] +version = "1.0" +features = [ + "derive", + "rc", +] +optional = true + +[dev-dependencies.evalexpr] +version = "13.1.0" + +[dev-dependencies.mail-parser] +version = "0.11" +features = [ + "full_encoding", + "serde", + "rkyv", +] + +[dev-dependencies.serde] +version = "1.0" +features = [ + "derive", + "rc", +] + +[dev-dependencies.serde_json] +version = "1.0" diff --git a/vendor/sieve-rs/LICENSES/AGPL-3.0-only.txt b/vendor/sieve-rs/LICENSES/AGPL-3.0-only.txt new file mode 100644 index 0000000..0c97efd --- /dev/null +++ b/vendor/sieve-rs/LICENSES/AGPL-3.0-only.txt @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/vendor/sieve-rs/LICENSES/LicenseRef-SEL.txt b/vendor/sieve-rs/LICENSES/LicenseRef-SEL.txt new file mode 100644 index 0000000..79b44ae --- /dev/null +++ b/vendor/sieve-rs/LICENSES/LicenseRef-SEL.txt @@ -0,0 +1,101 @@ +Stalwart Enterprise License 1.0 (SELv1) Agreement +================================================= + +Last Update: July 8, 2024 + +PLEASE CAREFULLY READ THIS STALWART ENTERPRISE LICENSE AGREEMENT ("AGREEMENT"). THIS AGREEMENT CONSTITUTES A LEGALLY BINDING AGREEMENT BETWEEN YOU AND STALWART LABS LTD AND GOVERNS YOUR USE OF THE SOFTWARE (DEFINED BELOW). IF YOU DO NOT AGREE WITH THIS AGREEMENT, YOU MAY NOT USE THE SOFTWARE. IF YOU ARE USING THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE AUTHORITY TO AGREE TO THIS AGREEMENT ON BEHALF OF SUCH ENTITY. IF YOU DO NOT HAVE SUCH AUTHORITY, DO NOT USE THE SOFTWARE IN ANY MANNER. + +This Agreement is entered into by and between Stalwart Labs Ltd and you, or the legal entity on behalf of whom you are acting. + +1. DEFINITIONS + +1.1. "Software" refers to the Stalwart Mail Server Enterprise Edition software, including all its versions, updates, modifications, accompanying documentation, and related materials. +1.2. "Subscription" refers to the paid access to the Software provided by Licensor to Licensee. +1.3. "Licensor" refers to Stalwart Labs Ltd, the entity providing the Software. +1.4. "Licensee" refers to the individual or entity installing, accessing, or using the Software with a valid Subscription. +1.5. "License Key" refers to the unique code provided by Licensor upon purchasing a Subscription which activates the full features of the Software. +1.6. "Source Code" refers to the human-readable version of the Software's code, as opposed to the compiled machine-readable version. + +2. GRANT OF LICENSE + +2.1. Licensor grants Licensee a revocable, non-exclusive, non-transferable, non-sublicensable, limited license to download, install, and use the Software. +2.2. The use of the Software is conditioned upon Licensee maintaining an active and valid paid subscription with Licensor. The paid subscription covers all versions of the Software and all updates and modifications. +2.3. This license grants Licensee the right to use the Software for both personal and commercial purposes. However, Licensee is expressly prohibited from reselling, leasing, sublicensing, or otherwise redistributing the Software itself. +2.4. This license is further governed by the terms and conditions set forth in any licensing agreements separately executed between Licensor and Licensee. In the event of any conflict between the terms of this Agreement and the terms of a signed licensing agreement, the terms of the signed licensing agreement shall control. +2.5. You are not granted any other rights beyond what is expressly stated herein. + +3. LICENSE KEYS + +3.1. The Software shall not be used without a valid License Key issued by Licensor. +3.2. Licensee is required to use valid License Keys issued by Licensor to run the Software, including any modified versions. Any attempts to bypass the License Key requirement is a violation of this Agreement. +3.3. Distribution or sharing of License Keys to third parties, not associated with Licensee, is strictly prohibited. +3.4. License Keys are bound to the subscription period. Should your subscription expire, all License Keys will become invalid after 15 days from the subscription expiration date. +3.5. Any instance of the Software using such an expired key will revert to the Community Edition functionality after the aforementioned 15-day period. + +4. SOURCE CODE USAGE + +4.1. Licensee is permitted to view, copy, and modify the Software's Source Code, as made available by Licensor, solely for Licensee's internal business use and in compliance with this Agreement's terms. +4.2. Any modifications to the Source Code do not grant Licensee any ownership rights to the original Software or any modifications. All rights, title, and interest to the Software and its Source Code remain exclusively with Licensor. +4.3. Licensee is strictly prohibited from altering, removing, or in any way tampering with the License Key validation system within the Software. Any such unauthorized modifications will be considered a material breach of this Agreement and may result in legal action. +4.3. Notwithstanding the availability of the Software's Source Code for review and limited modification, the Software and its Source Code are not open source and remain proprietary to Licensor. The provision of access to the Source Code does not confer any rights typically associated with open source software, including but not limited to the right to freely sublicense, or create derivative works for public distribution. All rights not expressly granted herein are reserved by Licensor. +4.4. Notwithstanding the foregoing, you may copy the Source Code for development and testing purposes, without requiring a Subscription. + +5. INTELLECTUAL PROPERTY RIGHTS + +5.1. The Licensor retains all rights, title, and interest in and to the Software, including all intellectual property rights therein. This Agreement does not transfer any ownership rights to the Licensee. +5.2. The Licensee must not remove, alter, or obscure any proprietary notices (including copyright and trademark notices) on the Software. + +6. TERMINATION + +6.1. Licensor reserves the right to terminate this Agreement immediately if the Licensee fails to comply with any terms and conditions of this Agreement. +6.2. In the event of a termination, you will be provided with a written notice, sent to the email address used during your subscription to the Software, outlining the reasons for the termination. +6.3. Upon termination, all rights granted to you under this Agreement will cease, and you must promptly cease all use of the Software. + +7. LIMITATION OF LIABILITY + +In no event will the Licensor be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from (i) your use or inability to use the Software; (ii) any unauthorized access to or use of our servers and/or any personal information stored therein. + +8. GOVERNING LAW & JURISDICTION + +This Agreement shall be governed by and construed under the laws of the United Kingdom. Any disputes arising from or related to this Agreement shall be resolved in the jurisdiction of London, UK. + +9. DATA PROTECTION & PRIVACY + +By using the Software, you consent to the collection, processing, and use of any personal data as required for the functionality of the Software. The specifics of data handling and storage will be outlined in the company's Privacy Policy, which can be accessed on the company's website. + +10. ACCEPTANCE + +By downloading, installing, or using the Software software, even without explicitly clicking on an "I Agree" button or a similar mechanism, you acknowledge that you have read, understood, and agreed to be bound by the terms and conditions of this Agreement. + +11. ASSIGNMENT + +This Agreement and the rights granted hereunder may not be transferred or assigned by you but may be assigned by Licensor without restriction. + +12. SEVERABILITY + +If any provision of this Agreement is held to be unenforceable or invalid for any reason, that provision shall be reformed to the extent necessary to make it enforceable and consistent with the intent of the parties, and the remaining provisions shall remain in full force and effect. + +13. ENTIRE AGREEMENT + +This Agreement constitutes the entire agreement between the Licensor and the Licensee with respect to the subject matter hereof and supersedes all prior or contemporaneous understandings regarding such subject matter. No amendment to or modification of this Agreement will be binding unless in writing and signed by the Licensor. + +14. DISCLAIMERS AND WARRANTIES + +The Software is provided "AS IS" and "AS AVAILABLE", without warranty of any kind, either express or implied, including, without limitation, warranties of merchantability, fitness for a particular purpose, and non-infringement. Licensor does not warrant that the Software will be error-free, that access thereto will be uninterrupted, or that defects will be corrected. + +15. INDEMNIFICATION + +Licensee agrees to indemnify, defend, and hold harmless Licensor, its officers, directors, employees, agents, licensors, suppliers, and any third-party information providers from and against all claims, losses, expenses, damages, and costs, including reasonable attorneys' fees, resulting from any violation of this Agreement or any activity related to your use or misuse of the Software (including negligent or wrongful conduct). + +16. FORCE MAJEURE + +Neither party shall be in default or otherwise liable for any delay in or failure of its performance under this Agreement if such delay or failure arises by any reason of any event beyond the reasonable control of a party, including acts of God, the elements, earthquakes, floods, fires, epidemics, riots, failures or delays in transportation or communications, or any act or failure to act by the other party or such other party’s officers, employees, agents, or contractors. The parties will promptly inform and consult with each other as to any of the above causes which, in their judgment, may or could be the cause of a delay in the performance of this Agreement. + +17. CONTACT INFORMATION + +If you have any questions about this Agreement, please contact Stalwart Labs Ltd. at: + +Stalwart Labs Ltd. +128 City Road +London, United Kingdom +hello@stalw.art diff --git a/vendor/sieve-rs/README.md b/vendor/sieve-rs/README.md new file mode 100644 index 0000000..79de01a --- /dev/null +++ b/vendor/sieve-rs/README.md @@ -0,0 +1,242 @@ +# sieve + +[![crates.io](https://img.shields.io/crates/v/sieve-rs)](https://crates.io/crates/sieve-rs) +[![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml) +[![docs.rs](https://img.shields.io/docsrs/sieve-rs)](https://docs.rs/sieve-rs) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) + +_sieve_ is a fast and secure Sieve filter interpreter for Rust that supports all [registered Sieve extensions](https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml). + +## Usage Example + +```rust +use sieve::{runtime::RuntimeError, Action, Compiler, Event, Input, Runtime}; + +// Sieve script to execute +let text_script = br#" +require ["fileinto", "body", "imap4flags"]; + +if body :contains "tps" { + setflag "$tps_reports"; +} + +if header :matches "List-ID" "*<*@*" { + fileinto "INBOX.lists.${2}"; stop; +} +"#; + +// Message to filter +let raw_message = r#"From: Sales Mailing List +To: John Doe +List-ID: +Subject: TPS Reports + +We're putting new coversheets on all the TPS reports before they go out now. +So if you could go ahead and try to remember to do that from now on, that'd be great. All right! +"#; + +// Compile +let compiler = Compiler::new(); +let script = compiler.compile(text_script).unwrap(); + +// Build runtime +let runtime = Runtime::new(); + +// Create filter instance +let mut instance = runtime.filter(raw_message.as_bytes()); +let mut input = Input::script("my-script", script); +let mut messages: Vec = Vec::new(); + +// Start event loop +while let Some(result) = instance.run(input) { + match result { + Ok(event) => match event { + Event::IncludeScript { name, optional } => { + // NOTE: Just for demonstration purposes, script name needs to be validated first. + if let Ok(bytes) = std::fs::read(name.as_str()) { + let script = compiler.compile(&bytes).unwrap(); + input = Input::script(name, script); + } else if optional { + input = Input::False; + } else { + panic!("Script {} not found.", name); + } + } + Event::MailboxExists { .. } => { + // Set to true if the mailbox exists + input = false.into(); + } + Event::ListContains { .. } => { + // Set to true if the list(s) contains an entry + input = false.into(); + } + Event::DuplicateId { .. } => { + // Set to true if the ID is duplicate + input = false.into(); + } + Event::Execute { command, arguments } => { + println!( + "Script executed command {:?} with parameters {:?}", + command, arguments + ); + // Set to true if the script succeeded + input = false.into(); + } + + Event::Keep { flags, message_id } => { + println!( + "Keep message '{}' with flags {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + flags + ); + input = true.into(); + } + Event::Discard => { + println!("Discard message."); + input = true.into(); + } + Event::Reject { reason, .. } => { + println!("Reject message with reason {:?}.", reason); + input = true.into(); + } + Event::FileInto { + folder, + flags, + message_id, + .. + } => { + println!( + "File message '{}' in folder {:?} with flags {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + folder, + flags + ); + input = true.into(); + } + Event::SendMessage { + recipient, + message_id, + .. + } => { + println!( + "Send message '{}' to {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + recipient + ); + input = true.into(); + } + Event::Notify { + message, method, .. + } => { + println!("Notify URI {:?} with message {:?}", method, message); + input = true.into(); + } + Event::CreatedMessage { message, .. } => { + messages.push(String::from_utf8(message).unwrap()); + input = true.into(); + } + + #[cfg(test)] + _ => unreachable!(), + }, + Err(error) => { + match error { + RuntimeError::TooManyIncludes => { + eprintln!("Too many included scripts."); + } + RuntimeError::InvalidInstruction(instruction) => { + eprintln!( + "Invalid instruction {:?} found at {}:{}.", + instruction.name(), + instruction.line_num(), + instruction.line_pos() + ); + } + RuntimeError::ScriptErrorMessage(message) => { + eprintln!("Script called the 'error' function with {:?}", message); + } + RuntimeError::CapabilityNotAllowed(capability) => { + eprintln!( + "Capability {:?} has been disabled by the administrator.", + capability + ); + } + RuntimeError::CapabilityNotSupported(capability) => { + eprintln!("Capability {:?} not supported.", capability); + } + RuntimeError::CPULimitReached => { + eprintln!("Script exceeded the configured CPU limit."); + } + } + input = true.into(); + } + } +} +``` + +## Testing & Fuzzing + +To run the testsuite: + +```bash + $ cargo test --all-features +``` + +To fuzz the library with `cargo-fuzz`: + +```bash + $ cargo +nightly fuzz run sieve +``` + +## Conformed RFCs + +- [RFC 5228 - Sieve: An Email Filtering Language](https://datatracker.ietf.org/doc/html/rfc5228) +- [RFC 3894 - Copying Without Side Effects](https://datatracker.ietf.org/doc/html/rfc3894) +- [RFC 5173 - Body Extension](https://datatracker.ietf.org/doc/html/rfc5173) +- [RFC 5183 - Environment Extension](https://datatracker.ietf.org/doc/html/rfc5183) +- [RFC 5229 - Variables Extension](https://datatracker.ietf.org/doc/html/rfc5229) +- [RFC 5230 - Vacation Extension](https://datatracker.ietf.org/doc/html/rfc5230) +- [RFC 5231 - Relational Extension](https://datatracker.ietf.org/doc/html/rfc5231) +- [RFC 5232 - Imap4flags Extension](https://datatracker.ietf.org/doc/html/rfc5232) +- [RFC 5233 - Subaddress Extension](https://datatracker.ietf.org/doc/html/rfc5233) +- [RFC 5235 - Spamtest and Virustest Extensions](https://datatracker.ietf.org/doc/html/rfc5235) +- [RFC 5260 - Date and Index Extensions](https://datatracker.ietf.org/doc/html/rfc5260) +- [RFC 5293 - Editheader Extension](https://datatracker.ietf.org/doc/html/rfc5293) +- [RFC 5429 - Reject and Extended Reject Extensions](https://datatracker.ietf.org/doc/html/rfc5429) +- [RFC 5435 - Extension for Notifications](https://datatracker.ietf.org/doc/html/rfc5435) +- [RFC 5463 - Ihave Extension](https://datatracker.ietf.org/doc/html/rfc5463) +- [RFC 5490 - Extensions for Checking Mailbox Status and Accessing Mailbox Metadata](https://datatracker.ietf.org/doc/html/rfc5490) +- [RFC 5703 - MIME Part Tests, Iteration, Extraction, Replacement, and Enclosure](https://datatracker.ietf.org/doc/html/rfc5703) +- [RFC 6009 - Delivery Status Notifications and Deliver-By Extensions](https://datatracker.ietf.org/doc/html/rfc6009) +- [RFC 6131 - Sieve Vacation Extension: "Seconds" Parameter](https://datatracker.ietf.org/doc/html/rfc6131) +- [RFC 6134 - Externally Stored Lists](https://datatracker.ietf.org/doc/html/rfc6134) +- [RFC 6558 - Converting Messages before Delivery](https://datatracker.ietf.org/doc/html/rfc6558) +- [RFC 6609 - Include Extension](https://datatracker.ietf.org/doc/html/rfc6609) +- [RFC 7352 - Detecting Duplicate Deliveries](https://datatracker.ietf.org/doc/html/rfc7352) +- [RFC 8579 - Delivering to Special-Use Mailboxes](https://datatracker.ietf.org/doc/html/rfc8579) +- [RFC 8580 - File Carbon Copy (FCC)](https://datatracker.ietf.org/doc/html/rfc8580) +- [RFC 9042 - Delivery by MAILBOXID](https://datatracker.ietf.org/doc/html/rfc9042) +- [REGEX-01 - Regular Expression Extension (draft-ietf-sieve-regex-01)](https://www.ietf.org/archive/id/draft-ietf-sieve-regex-01.html) + +## License + +Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by +the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +You can be released from the requirements of the AGPLv3 license by purchasing a commercial license. Please contact licensing@stalw.art for more details. + +## Copyright + +Copyright (C) 2020, Stalwart Labs LLC diff --git a/vendor/sieve-rs/VENDORED.md b/vendor/sieve-rs/VENDORED.md new file mode 100644 index 0000000..2cdf7a6 --- /dev/null +++ b/vendor/sieve-rs/VENDORED.md @@ -0,0 +1,33 @@ +# sieve-rs, vendored + +The published [sieve-rs](https://crates.io/crates/sieve-rs) **0.7.3**, taken +from crates.io under AGPL-3.0-only, with the upstream project's name taken +out of the identifiers it spells into the Sieve language (docs/spec/SPEC.md +§2.4). The root `Cargo.toml` patches it in with `[patch.crates-io]`. + +## Changes + +Every changed file carries the AGPL 5(a) notice in its header. + +- `src/compiler/grammar/mod.rs`: the extensions scripts `require` and + ManageSieve advertises are `vnd.inbuxa.while` and + `vnd.inbuxa.expressions`. +- `src/runtime/mod.rs`: the default `environment "name"` is `inbuxa Sieve` + (the server sets its own name on both of its runtimes anyway), and the + test-only capability is `vnd.inbuxa.testsuite`. +- `src/lib.rs`: the test-suite environment variables are `vnd.inbuxa.*`. +- `Cargo.toml`: the example target is dropped, with the top-level + `examples/` and `tests/`, which the server never builds. (`src/` keeps its + own `tests` modules: the crate declares them.) + +## Updating + +When the server's `Cargo.lock` moves sieve-rs to a new version, Cargo stops +using this copy and only warns that the patch went unused. The unit test +`sieve_extensions_carry_the_fork_name` in `crates/common` fails when that +happens. Re-vendor the new version from `~/.cargo/registry/src/*/sieve-rs-*`, +dropping the top-level `tests/` and `examples/` (only those: `rsync +--exclude /tests --exclude /examples`), `Cargo.lock` and `Cargo.toml.orig`, repeat +the changes above, and update the version here and in the root `Cargo.toml` +comment. `tools/fork/name-check.py` covers `vendor/` too, so a rename missed +in a new version fails CI. diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_convert.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_convert.rs new file mode 100644 index 0000000..1b3b777 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_convert.rs @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{ + instruction::{CompilerState, Instruction}, + test::Test, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Convert { + pub from_media_type: Value, + pub to_media_type: Value, + pub transcoding_params: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_convert(&mut self) -> Result { + Ok(Test::Convert(Convert { + from_media_type: self.parse_string()?, + to_media_type: self.parse_string()?, + transcoding_params: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_convert(&mut self) -> Result<(), CompileError> { + let cmd = Instruction::Convert(Convert { + from_media_type: self.parse_string()?, + to_media_type: self.parse_string()?, + transcoding_params: self.parse_strings(false)?, + is_not: false, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_editheader.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_editheader.rs new file mode 100644 index 0000000..20d1679 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_editheader.rs @@ -0,0 +1,212 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::HeaderName; + +use crate::compiler::{ + CompileError, ErrorType, Value, + grammar::{ + Capability, Comparator, + instruction::{CompilerState, Instruction}, + }, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::MatchType; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct AddHeader { + pub last: bool, + pub field_name: Value, + pub value: Value, +} + +/* + Usage: "deleteheader" [":index" [":last"]] + [COMPARATOR] [MATCH-TYPE] + + [] + +*/ +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct DeleteHeader { + pub index: Option, + pub comparator: Comparator, + pub match_type: MatchType, + pub field_name: Value, + pub value_patterns: Vec, + pub mime_anychild: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_addheader(&mut self) -> Result<(), CompileError> { + let mut field_name = None; + let value; + let mut last = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Last) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + last = true; + } + _ => { + let string = self.parse_string_token(token_info)?; + if field_name.is_none() { + if let Value::Text(header_name) = &string + && HeaderName::parse(header_name.as_ref()).is_none() + { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + + field_name = string.into(); + } else { + if matches!( + &string, + Value::Text(value) if value.len() > self.compiler.max_header_size + ) { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::HeaderTooLong)); + } + value = string; + break; + } + } + } + } + self.instructions.push(Instruction::AddHeader(AddHeader { + last, + field_name: field_name.unwrap(), + value, + })); + Ok(()) + } + + pub(crate) fn parse_deleteheader(&mut self) -> Result<(), CompileError> { + let field_name: Value; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut index = None; + let mut index_last = false; + let mut mime = false; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 5, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + _ => { + field_name = self.parse_string_token(token_info)?; + if let Value::Text(header_name) = &field_name + && HeaderName::parse(header_name.as_ref()).is_none() + { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + break; + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + + let cmd = Instruction::DeleteHeader(DeleteHeader { + index: if index_last { index.map(|i| -i) } else { index }, + comparator, + match_type, + field_name, + value_patterns: if let Some(Ok( + Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen, + )) = self.tokens.peek().map(|r| r.map(|t| &t.token)) + { + let mut key_list = self.parse_strings(false)?; + self.validate_match(&match_type, &mut key_list)?; + key_list + } else { + Vec::new() + }, + mime_anychild, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_fileinto.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_fileinto.rs new file mode 100644 index 0000000..942a6f5 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_fileinto.rs @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{ + Capability, + instruction::{CompilerState, Instruction}, + }, + lexer::{Token, word::Word}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct FileInto { + pub copy: bool, + pub create: bool, + pub folder: Value, + pub flags: Vec, + pub mailbox_id: Option, + pub special_use: Option, +} + +impl CompilerState<'_> { + pub(crate) fn parse_fileinto(&mut self) -> Result<(), CompileError> { + let folder; + let mut copy = false; + let mut create = false; + let mut flags = Vec::new(); + let mut mailbox_id = None; + let mut special_use = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Copy) => { + self.validate_argument( + 1, + Capability::Copy.into(), + token_info.line_num, + token_info.line_pos, + )?; + copy = true; + } + Token::Tag(Word::Create) => { + self.validate_argument( + 2, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + create = true; + } + Token::Tag(Word::Flags) => { + self.validate_argument( + 3, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + flags = self.parse_strings(false)?; + } + Token::Tag(Word::MailboxId) => { + self.validate_argument( + 4, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + mailbox_id = self.parse_string()?.into(); + } + Token::Tag(Word::SpecialUse) => { + self.validate_argument( + 5, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + special_use = self.parse_string()?.into(); + } + _ => { + folder = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::FileInto(FileInto { + folder, + copy, + create, + flags, + mailbox_id, + special_use, + })); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_flags.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_flags.rs new file mode 100644 index 0000000..745a267 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_flags.rs @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, ErrorType, Value, VariableType, + grammar::instruction::{CompilerState, Instruction}, + lexer::{Token, word::Word}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct EditFlags { + pub action: Action, + pub name: Option, + pub flags: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Action { + Set, + Add, + Remove, +} + +impl CompilerState<'_> { + pub(crate) fn parse_flag_action(&mut self, word: Word) -> Result<(), CompileError> { + let token_info = self.tokens.unwrap_next()?; + let action = match word { + Word::SetFlag => Action::Set, + Word::AddFlag => Action::Add, + Word::RemoveFlag => Action::Remove, + _ => unreachable!(), + }; + + let instruction = Instruction::EditFlags( + match ( + &token_info.token, + self.tokens.peek().map(|r| r.map(|t| &t.token)), + ) { + ( + Token::StringConstant(_), + Some(Ok( + Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen, + )), + ) => EditFlags { + name: self.parse_variable_name(token_info, false)?.into(), + flags: self.parse_strings(false)?, + action, + }, + (Token::BracketOpen, _) + | ( + Token::StringConstant(_) | Token::StringVariable(_), + Some(Ok(Token::Semicolon)), + ) => EditFlags { + name: None, + flags: self.parse_strings_token(token_info)?, + action, + }, + _ => { + return Err(token_info.custom(ErrorType::InvalidArguments)); + } + }, + ); + + self.instructions.push(instruction); + + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_include.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_include.rs new file mode 100644 index 0000000..41a03e6 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_include.rs @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::instruction::{CompilerState, Instruction}, + lexer::{Token, word::Word}, +}; + +/* + +include [LOCATION] [":once"] [":optional"] + LOCATION = ":personal" / ":global" + +*/ + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Include { + pub location: Location, + pub once: bool, + pub optional: bool, + pub value: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Location { + Personal, + Global, +} + +impl CompilerState<'_> { + pub(crate) fn parse_include(&mut self) -> Result<(), CompileError> { + let value; + let mut once = false; + let mut optional = false; + let mut location = Location::Personal; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Once) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + once = true; + } + Token::Tag(Word::Optional) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + optional = true; + } + Token::Tag(Word::Personal) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + location = Location::Personal; + } + Token::Tag(Word::Global) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + location = Location::Global; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Include(Include { + location, + once, + optional, + value, + })); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_keep.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_keep.rs new file mode 100644 index 0000000..8af550b --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_keep.rs @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{ + Capability, + instruction::{CompilerState, Instruction}, + }, + lexer::{Token, word::Word}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Keep { + pub flags: Vec, +} + +impl CompilerState<'_> { + pub(crate) fn parse_keep(&mut self) -> Result<(), CompileError> { + let cmd = Instruction::Keep(Keep { + flags: match self.tokens.peek().map(|r| r.map(|t| &t.token)) { + Some(Ok(Token::Tag(Word::Flags))) => { + let token_info = self.tokens.next().unwrap().unwrap(); + self.validate_argument( + 0, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_strings(false)? + } + _ => Vec::new(), + }, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_mime.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_mime.rs new file mode 100644 index 0000000..fdbaa1f --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_mime.rs @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, VariableType, + grammar::instruction::{CompilerState, Instruction}, + lexer::{Token, word::Word}, +}; + +use super::action_set::Modifier; + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct ForEveryPart { + pub jz_pos: usize, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Replace { + pub subject: Option, + pub from: Option, + pub replacement: Value, + pub mime: bool, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Enclose { + pub subject: Option, + pub headers: Vec, + pub value: Value, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct ExtractText { + pub modifiers: Vec, + pub first: Option, + pub name: VariableType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum MimeOpts { + Type, + Subtype, + ContentType, + Param(Vec), + None, +} + +impl CompilerState<'_> { + pub(crate) fn parse_replace(&mut self) -> Result<(), CompileError> { + let mut subject = None; + let mut from = None; + let replacement; + let mut mime = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Mime) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + mime = true; + } + Token::Tag(Word::Subject) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + subject = self.parse_string()?.into(); + } + Token::Tag(Word::From) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + from = self.parse_string()?.into(); + } + _ => { + replacement = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Replace(Replace { + subject, + from, + replacement, + mime, + })); + Ok(()) + } + + pub(crate) fn parse_enclose(&mut self) -> Result<(), CompileError> { + let mut subject = None; + let mut headers = Vec::new(); + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Subject) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + subject = self.parse_string()?.into(); + } + Token::Tag(Word::Headers) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + headers = self.parse_strings(false)?; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Enclose(Enclose { + subject, + headers, + value, + })); + Ok(()) + } + + pub(crate) fn parse_extracttext(&mut self) -> Result<(), CompileError> { + let mut modifiers = Vec::new(); + let mut first = None; + let name; + let mut is_local = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::First) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + first = self.tokens.expect_number(usize::MAX)?.into(); + } + Token::Tag( + word @ (Word::Lower + | Word::Upper + | Word::LowerFirst + | Word::UpperFirst + | Word::QuoteWildcard + | Word::QuoteRegex + | Word::Length), + ) => { + let modifier = word.into(); + if !modifiers.contains(&modifier) { + modifiers.push(modifier); + } + } + Token::Tag(Word::Replace) => { + let find = self.tokens.unwrap_next()?; + let replace = self.tokens.unwrap_next()?; + modifiers.push(Modifier::Replace { + find: self.parse_string_token(find)?, + replace: self.parse_string_token(replace)?, + }); + } + Token::Tag(Word::Local) => { + is_local = true; + } + _ => { + name = self.parse_variable_name(token_info, is_local)?; + break; + } + } + } + + modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order())); + + self.instructions + .push(Instruction::ExtractText(ExtractText { + modifiers, + first, + name, + })); + Ok(()) + } + + pub(crate) fn parse_mimeopts(&mut self, opts: Word) -> Result, CompileError> { + Ok(match opts { + Word::Type => MimeOpts::Type, + Word::Subtype => MimeOpts::Subtype, + Word::ContentType => MimeOpts::ContentType, + Word::Param => MimeOpts::Param(self.parse_strings(false)?), + _ => MimeOpts::None, + }) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_notify.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_notify.rs new file mode 100644 index 0000000..a7ec440 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_notify.rs @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + FileCarbonCopy, + compiler::{ + CompileError, ErrorType, Value, + grammar::{ + Capability, + instruction::{CompilerState, Instruction, MapLocalVars}, + }, + lexer::{Token, word::Word}, + }, + runtime::actions::action_notify::{validate_from, validate_uri}, +}; + +/* +notify [":from" string] + [":importance" <"1" / "2" / "3">] + [":options" string-list] + [":message" string] + + +*/ + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Notify { + pub from: Option, + pub importance: Option, + pub options: Vec, + pub message: Option, + pub fcc: Option>, + pub method: Value, +} + +impl CompilerState<'_> { + pub(crate) fn parse_notify(&mut self) -> Result<(), CompileError> { + let method; + let mut from = None; + let mut importance = None; + let mut message = None; + let mut options = Vec::new(); + + let mut fcc = None; + let mut create = false; + let mut flags = Vec::new(); + let mut special_use = None; + let mut mailbox_id = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::From) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + let address = self.parse_string()?; + if let Value::Text(address) = &address + && (address.is_empty() || !validate_from(address)) + { + return Err(token_info.custom(ErrorType::InvalidAddress)); + } + from = address.into(); + } + Token::Tag(Word::Message) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + message = self.parse_string()?.into(); + } + Token::Tag(Word::Importance) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + importance = self.parse_string()?.into(); + } + Token::Tag(Word::Options) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + options = self.parse_strings(false)?; + } + Token::Tag(Word::Create) => { + self.validate_argument( + 5, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + create = true; + } + Token::Tag(Word::SpecialUse) => { + self.validate_argument( + 6, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + special_use = self.parse_string()?.into(); + } + Token::Tag(Word::MailboxId) => { + self.validate_argument( + 7, + Capability::MailboxId.into(), + token_info.line_num, + token_info.line_pos, + )?; + mailbox_id = self.parse_string()?.into(); + } + Token::Tag(Word::Fcc) => { + self.validate_argument( + 8, + Capability::Fcc.into(), + token_info.line_num, + token_info.line_pos, + )?; + fcc = self.parse_string()?.into(); + } + Token::Tag(Word::Flags) => { + self.validate_argument( + 9, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + flags = self.parse_strings(false)?; + } + _ => { + if let Token::StringConstant(uri) = &token_info.token + && validate_uri(uri.to_string().as_ref()).is_none() + { + return Err(token_info.custom(ErrorType::InvalidURI)); + } + + method = self.parse_string_token(token_info)?; + break; + } + } + } + + if fcc.is_none() + && (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some()) + { + return Err(self.tokens.unwrap_next()?.missing_tag(":fcc")); + } + + self.instructions.push(Instruction::Notify(Notify { + method, + from, + importance, + options, + message, + fcc: if let Some(fcc) = fcc { + FileCarbonCopy { + mailbox: fcc, + create, + flags, + special_use, + mailbox_id, + } + .into() + } else { + None + }, + })); + Ok(()) + } +} + +impl MapLocalVars for FileCarbonCopy { + fn map_local_vars(&mut self, last_id: usize) { + self.mailbox.map_local_vars(last_id); + self.mailbox_id.map_local_vars(last_id); + self.flags.map_local_vars(last_id); + self.special_use.map_local_vars(last_id); + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_redirect.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_redirect.rs new file mode 100644 index 0000000..08bb8e9 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_redirect.rs @@ -0,0 +1,288 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{ + Capability, + instruction::{CompilerState, Instruction, MapLocalVars}, + }, + lexer::{Token, word::Word}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Redirect { + pub copy: bool, + pub address: Value, + pub notify: Notify, + pub return_of_content: Ret, + pub by_time: ByTime, + pub list: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum NotifyItem { + Success, + Failure, + Delay, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Notify { + Never, + Items(Vec), + Default, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Ret { + Full, + Hdrs, + Default, +} + +/* + + Usage: redirect [:bytimerelative / + :bytimeabsolute + [:bymode "notify"|"return"] [:bytrace]] + + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum ByTime { + Relative { + rlimit: u64, + mode: ByMode, + trace: bool, + }, + Absolute { + alimit: T, + mode: ByMode, + trace: bool, + }, + None, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum ByMode { + Notify, + Return, + Default, +} + +impl CompilerState<'_> { + pub(crate) fn parse_redirect(&mut self) -> Result<(), CompileError> { + let address; + let mut copy = false; + let mut ret = Ret::Default; + let mut notify = Notify::Default; + let mut list = false; + let mut by_mode = ByMode::Default; + let mut by_trace = false; + let mut by_rlimit = None; + let mut by_alimit = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Copy) => { + self.validate_argument( + 1, + Capability::Copy.into(), + token_info.line_num, + token_info.line_pos, + )?; + copy = true; + } + Token::Tag(Word::List) => { + self.validate_argument( + 2, + Capability::ExtLists.into(), + token_info.line_num, + token_info.line_pos, + )?; + list = true; + } + Token::Tag(Word::ByTrace) => { + self.validate_argument( + 3, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + by_trace = true; + } + Token::Tag(Word::ByMode) => { + self.validate_argument( + 4, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + let by_mode_ = self.tokens.expect_static_string()?; + if by_mode_.eq_ignore_ascii_case("notify") { + by_mode = ByMode::Notify; + } else if by_mode_.eq_ignore_ascii_case("return") { + by_mode = ByMode::Return; + } else { + return Err(token_info.expected("\"notify\" or \"return\"")); + } + } + Token::Tag(Word::ByTimeRelative) => { + self.validate_argument( + 5, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + by_rlimit = (self.tokens.expect_number(u64::MAX as usize)? as u64).into(); + } + Token::Tag(Word::ByTimeAbsolute) => { + self.validate_argument( + 5, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + by_alimit = self.parse_string()?.into(); + } + Token::Tag(Word::Ret) => { + self.validate_argument( + 6, + Capability::RedirectDsn.into(), + token_info.line_num, + token_info.line_pos, + )?; + let ret_ = self.tokens.expect_static_string()?; + if ret_.eq_ignore_ascii_case("full") { + ret = Ret::Full; + } else if ret_.eq_ignore_ascii_case("hdrs") { + ret = Ret::Hdrs; + } else { + return Err(token_info.expected("\"FULL\" or \"HDRS\"")); + } + } + Token::Tag(Word::Notify) => { + self.validate_argument( + 7, + Capability::RedirectDsn.into(), + token_info.line_num, + token_info.line_pos, + )?; + let notify_ = self.tokens.expect_static_string()?; + if notify_.eq_ignore_ascii_case("never") { + notify = Notify::Never; + } else { + let mut items = Vec::new(); + for item in notify_.split(',') { + let item = item.trim(); + if item.eq_ignore_ascii_case("success") { + items.push(NotifyItem::Success); + } else if item.eq_ignore_ascii_case("failure") { + items.push(NotifyItem::Failure); + } else if item.eq_ignore_ascii_case("delay") { + items.push(NotifyItem::Delay); + } + } + if !items.is_empty() { + notify = Notify::Items(items); + } else { + return Err( + token_info.expected("\"NEVER\" or \"SUCCESS, FAILURE, DELAY, ..\"") + ); + } + } + } + _ => { + address = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Redirect(Redirect { + address, + copy, + notify, + return_of_content: ret, + by_time: if let Some(alimit) = by_alimit { + ByTime::Absolute { + alimit, + mode: by_mode, + trace: by_trace, + } + } else if let Some(rlimit) = by_rlimit { + ByTime::Relative { + rlimit, + mode: by_mode, + trace: by_trace, + } + } else { + ByTime::None + }, + list, + })); + Ok(()) + } +} + +impl MapLocalVars for ByTime { + fn map_local_vars(&mut self, last_id: usize) { + if let ByTime::Absolute { alimit, .. } = self { + alimit.map_local_vars(last_id) + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_reject.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_reject.rs new file mode 100644 index 0000000..f99e71c --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_reject.rs @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::instruction::{CompilerState, Instruction}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Reject { + pub ereject: bool, + pub reason: Value, +} + +impl CompilerState<'_> { + pub(crate) fn parse_reject(&mut self, ereject: bool) -> Result<(), CompileError> { + let cmd = Instruction::Reject(Reject { + ereject, + reason: self.parse_string()?, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_require.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_require.rs new file mode 100644 index 0000000..d8db916 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_require.rs @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, + grammar::{ + Capability, + instruction::{CompilerState, Instruction}, + }, + lexer::Token, +}; + +impl CompilerState<'_> { + fn add_capability(&mut self, capabilities: &mut Vec, capability: Capability) { + if !self.has_capability(&capability) { + let parent_capability = if matches!(&capability, Capability::SpamTestPlus) { + Some(Capability::SpamTest) + } else { + None + }; + capabilities.push(capability.clone()); + self.block.capabilities.insert(capability); + + if let Some(capability) = parent_capability + && !self.has_capability(&capability) + { + capabilities.push(capability.clone()); + self.block.capabilities.insert(capability); + } + } + } + + pub(crate) fn parse_require(&mut self) -> Result<(), CompileError> { + let mut capabilities = Vec::new(); + + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::BracketOpen => loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(value) => { + self.add_capability( + &mut capabilities, + Capability::parse(value.to_string().as_ref()), + ); + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Comma => (), + Token::BracketClose => break, + _ => { + return Err(token_info.expected("']' or ','")); + } + } + } + _ => { + return Err(token_info.expected("string")); + } + } + }, + Token::StringConstant(value) => { + self.add_capability( + &mut capabilities, + Capability::parse(value.to_string().as_ref()), + ); + } + _ => { + return Err(token_info.expected("'[' or string")); + } + } + + if !capabilities.is_empty() { + if self.block.require_pos == usize::MAX { + self.block.require_pos = self.instructions.len(); + self.instructions.push(Instruction::Require(capabilities)); + } else if let Some(Instruction::Require(capabilties)) = + self.instructions.get_mut(self.block.require_pos) + { + capabilties.extend(capabilities) + } else { + #[cfg(test)] + panic!( + "Invalid require instruction position {}.", + self.block.require_pos + ) + } + } + + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_set.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_set.rs new file mode 100644 index 0000000..0e1b544 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_set.rs @@ -0,0 +1,207 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + Envelope, + compiler::{ + CompileError, ErrorType, Value, VariableType, + grammar::{ + expr::Expression, + instruction::{CompilerState, Instruction}, + }, + lexer::{Token, tokenizer::TokenInfo, word::Word}, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum Modifier { + Lower, + Upper, + LowerFirst, + UpperFirst, + QuoteWildcard, + QuoteRegex, + EncodeUrl, + Length, + Replace { find: Value, replace: Value }, +} + +impl Modifier { + pub fn order(&self) -> usize { + match self { + Modifier::Lower => 41, + Modifier::Upper => 40, + Modifier::LowerFirst => 31, + Modifier::UpperFirst => 30, + Modifier::QuoteWildcard => 20, + Modifier::QuoteRegex => 21, + Modifier::EncodeUrl => 15, + Modifier::Length => 10, + Modifier::Replace { .. } => 40, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Set { + pub modifiers: Vec, + pub name: VariableType, + pub value: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Let { + pub name: VariableType, + pub expr: Vec, +} + +impl CompilerState<'_> { + pub(crate) fn parse_set(&mut self) -> Result<(), CompileError> { + let mut modifiers = Vec::new(); + let mut name = None; + let mut is_local = false; + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Lower + | Word::Upper + | Word::LowerFirst + | Word::UpperFirst + | Word::QuoteWildcard + | Word::QuoteRegex + | Word::Length + | Word::EncodeUrl), + ) => { + let modifier = word.into(); + if !modifiers.contains(&modifier) { + modifiers.push(modifier); + } + } + Token::Tag(Word::Replace) => { + let find = self.tokens.unwrap_next()?; + let replace = self.tokens.unwrap_next()?; + modifiers.push(Modifier::Replace { + find: self.parse_string_token(find)?, + replace: self.parse_string_token(replace)?, + }); + } + Token::Tag(Word::Local) => { + is_local = true; + } + _ => { + if name.is_none() { + name = self.parse_variable_name(token_info, is_local)?.into(); + } else { + value = self.parse_string_token(token_info)?; + break; + } + } + } + } + + modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order())); + + self.instructions.push(Instruction::Set(Set { + modifiers, + name: name.unwrap(), + value, + })); + Ok(()) + } + + pub(crate) fn parse_let(&mut self) -> Result<(), CompileError> { + let name = self.tokens.unwrap_next()?; + let name = self.parse_variable_name(name, false)?; + let expr = self.parse_expr()?; + + self.instructions.push(Instruction::Let(Let { name, expr })); + Ok(()) + } + + pub(crate) fn parse_variable_name( + &mut self, + token_info: TokenInfo, + register_as_local: bool, + ) -> Result { + match token_info.token { + Token::StringConstant(value) => self + .register_variable(value.into_string(), register_as_local) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }), + _ => Err(token_info.custom(ErrorType::ExpectedConstantString)), + } + } + + pub(crate) fn register_variable( + &mut self, + name: String, + register_as_local: bool, + ) -> Result { + let name = name.to_lowercase(); + if let Some((namespace, part)) = name.split_once('.') { + match namespace { + "global" | "t" => Ok(VariableType::Global(part.to_string())), + "envelope" => Envelope::try_from(part) + .map(VariableType::Envelope) + .map_err(|_| ErrorType::InvalidNamespace(namespace.to_string())), + _ => Err(ErrorType::InvalidNamespace(namespace.to_string())), + } + } else { + Ok(if !self.is_var_global(&name) { + VariableType::Local(self.register_local_var(name, register_as_local)) + } else { + VariableType::Global(name) + }) + } + } +} + +impl From for Modifier { + fn from(word: Word) -> Self { + match word { + Word::Lower => Modifier::Lower, + Word::Upper => Modifier::Upper, + Word::LowerFirst => Modifier::LowerFirst, + Word::UpperFirst => Modifier::UpperFirst, + Word::QuoteWildcard => Modifier::QuoteWildcard, + Word::QuoteRegex => Modifier::QuoteRegex, + Word::Length => Modifier::Length, + Word::EncodeUrl => Modifier::EncodeUrl, + _ => unreachable!(), + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/action_vacation.rs b/vendor/sieve-rs/src/compiler/grammar/actions/action_vacation.rs new file mode 100644 index 0000000..fa95d64 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/action_vacation.rs @@ -0,0 +1,240 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + FileCarbonCopy, + compiler::{ + CompileError, Value, + grammar::{ + Capability, + instruction::{CompilerState, Instruction}, + test::Test, + }, + lexer::{Token, word::Word}, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Vacation { + pub subject: Option, + pub from: Option, + pub mime: bool, + pub fcc: Option>, + pub reason: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestVacation { + pub addresses: Vec, + pub period: Period, + pub handle: Option, + pub reason: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Period { + Days(u64), + Seconds(u64), + Default, +} + +/* + +vacation [":days" number] [":subject" string] + [":from" string] [":addresses" string-list] + [":mime"] [":handle" string] + +vacation [FCC] + [":days" number | ":seconds" number] + [":subject" string] + [":from" string] + [":addresses" string-list] + [":mime"] + [":handle" string] + + +":flags" + + + FCC = ":fcc" string *FCC-OPTS + ; per Section 2.6.2 of RFC 5228, + ; the tagged arguments in FCC may appear in any order + + FCC-OPTS = CREATE / IMAP-FLAGS / SPECIAL-USE + ; each option MUST NOT appear more than once + + CREATE = ":create" + IMAP-FLAGS = ":flags" string-list + SPECIAL-USE = ":specialuse" string +*/ + +impl CompilerState<'_> { + pub(crate) fn parse_vacation(&mut self) -> Result<(), CompileError> { + let mut period = Period::Default; + let mut subject = None; + let mut from = None; + let mut handle = None; + let mut addresses = Vec::new(); + let mut mime = false; + let reason; + + let mut fcc = None; + let mut create = false; + let mut flags = Vec::new(); + let mut special_use = None; + let mut mailbox_id = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Mime) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + mime = true; + } + Token::Tag(Word::Create) => { + self.validate_argument( + 2, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + create = true; + } + Token::Tag(Word::Days) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + period = Period::Days(self.tokens.expect_number(u64::MAX as usize)? as u64); + } + Token::Tag(Word::Seconds) => { + self.validate_argument( + 3, + Capability::VacationSeconds.into(), + token_info.line_num, + token_info.line_pos, + )?; + period = Period::Seconds(self.tokens.expect_number(u64::MAX as usize)? as u64); + } + Token::Tag(Word::Subject) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + subject = self.parse_string()?.into(); + } + Token::Tag(Word::From) => { + self.validate_argument(5, None, token_info.line_num, token_info.line_pos)?; + from = self.parse_string()?.into(); + } + Token::Tag(Word::Handle) => { + self.validate_argument(6, None, token_info.line_num, token_info.line_pos)?; + handle = self.parse_string()?.into(); + } + Token::Tag(Word::SpecialUse) => { + self.validate_argument( + 7, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + special_use = self.parse_string()?.into(); + } + Token::Tag(Word::MailboxId) => { + self.validate_argument( + 8, + Capability::MailboxId.into(), + token_info.line_num, + token_info.line_pos, + )?; + mailbox_id = self.parse_string()?.into(); + } + Token::Tag(Word::Fcc) => { + self.validate_argument( + 9, + Capability::Fcc.into(), + token_info.line_num, + token_info.line_pos, + )?; + fcc = self.parse_string()?.into(); + } + Token::Tag(Word::Flags) => { + self.validate_argument( + 10, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + flags = self.parse_strings(false)?; + } + Token::Tag(Word::Addresses) => { + self.validate_argument(11, None, token_info.line_num, token_info.line_pos)?; + addresses = self.parse_strings(false)?; + } + _ => { + reason = self.parse_string_token(token_info)?; + break; + } + } + } + + if fcc.is_none() + && (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some()) + { + return Err(self.tokens.unwrap_next()?.missing_tag(":fcc")); + } + + self.instructions + .push(Instruction::Test(Test::Vacation(TestVacation { + period, + handle, + reason: reason.clone(), + addresses, + }))); + + self.instructions + .push(Instruction::Jz(self.instructions.len() + 2)); + + self.instructions.push(Instruction::Vacation(Vacation { + reason, + subject, + from, + mime, + fcc: if let Some(fcc) = fcc { + FileCarbonCopy { + mailbox: fcc, + create, + flags, + special_use, + mailbox_id, + } + .into() + } else { + None + }, + })); + + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/actions/mod.rs b/vendor/sieve-rs/src/compiler/grammar/actions/mod.rs new file mode 100644 index 0000000..25487a8 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/actions/mod.rs @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod action_convert; +pub mod action_editheader; +pub mod action_fileinto; +pub mod action_flags; +pub mod action_include; +pub mod action_keep; +pub mod action_mime; +pub mod action_notify; +pub mod action_redirect; +pub mod action_reject; +pub mod action_require; +pub mod action_set; +pub mod action_vacation; diff --git a/vendor/sieve-rs/src/compiler/grammar/expr/mod.rs b/vendor/sieve-rs/src/compiler/grammar/expr/mod.rs new file mode 100644 index 0000000..0874ccf --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/expr/mod.rs @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::sync::Arc; + +use crate::compiler::{Number, VariableType}; + +pub mod parser; +pub mod tokenizer; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Expression { + Variable(VariableType), + Constant(Constant), + BinaryOperator(BinaryOperator), + UnaryOperator(UnaryOperator), + JmpIf { val: bool, pos: u32 }, + Function { id: u32, num_args: u32 }, + ArrayAccess, + ArrayBuild(u32), +} + +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Constant { + Integer(i64), + Float(f64), + String(Arc), +} + +impl Eq for Constant {} + +impl From for Constant { + fn from(value: Number) -> Self { + match value { + Number::Integer(i) => Constant::Integer(i), + Number::Float(f) => Constant::Float(f), + } + } +} + +impl From for Constant { + fn from(value: String) -> Self { + Constant::String(value.into()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum BinaryOperator { + Add, + Subtract, + Multiply, + Divide, + + And, + Or, + Xor, + + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum UnaryOperator { + Not, + Minus, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub(crate) enum Token { + Variable(VariableType), + Function { + name: String, + id: u32, + num_args: u32, + }, + Number(Number), + String(String), + BinaryOperator(BinaryOperator), + UnaryOperator(UnaryOperator), + OpenParen, + CloseParen, + OpenBracket, + CloseBracket, + Comma, +} diff --git a/vendor/sieve-rs/src/compiler/grammar/expr/parser.rs b/vendor/sieve-rs/src/compiler/grammar/expr/parser.rs new file mode 100644 index 0000000..8be48b2 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/expr/parser.rs @@ -0,0 +1,257 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use super::{BinaryOperator, Expression, Token, tokenizer::Tokenizer}; + +pub(crate) struct ExpressionParser<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + pub(crate) tokenizer: Tokenizer<'x, F>, + pub(crate) output: Vec, + operator_stack: Vec<(Token, Option)>, + arg_count: Vec, +} + +pub(crate) const ID_ARRAY_ACCESS: u32 = u32::MAX; +pub(crate) const ID_ARRAY_BUILD: u32 = u32::MAX - 1; +pub(crate) const ID_EXTERNAL: u32 = u32::MAX - 2; + +impl<'x, F> ExpressionParser<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + pub fn from_tokenizer(tokenizer: Tokenizer<'x, F>) -> Self { + Self { + tokenizer, + output: Vec::new(), + operator_stack: Vec::new(), + arg_count: Vec::new(), + } + } + + pub fn parse(mut self) -> Result { + let mut last_is_var_or_fnc = false; + + while let Some(token) = self.tokenizer.next()? { + let mut is_var_or_fnc = false; + match token { + Token::Variable(v) => { + self.inc_arg_count(); + is_var_or_fnc = true; + self.output.push(Expression::Variable(v)) + } + Token::Number(n) => { + self.inc_arg_count(); + self.output.push(Expression::Constant(n.into())) + } + Token::String(s) => { + self.inc_arg_count(); + self.output.push(Expression::Constant(s.into())) + } + Token::UnaryOperator(uop) => { + self.operator_stack.push((Token::UnaryOperator(uop), None)) + } + Token::OpenParen => self.operator_stack.push((token, None)), + Token::CloseParen | Token::CloseBracket => { + let expect_token = if matches!(token, Token::CloseParen) { + Token::OpenParen + } else { + Token::OpenBracket + }; + loop { + match self.operator_stack.pop() { + Some((t, _)) if t == expect_token => { + break; + } + Some((Token::BinaryOperator(bop), jmp_pos)) => { + self.update_jmp_pos(jmp_pos); + self.output.push(Expression::BinaryOperator(bop)) + } + Some((Token::UnaryOperator(uop), _)) => { + self.output.push(Expression::UnaryOperator(uop)) + } + _ => return Err("Mismatched parentheses".to_string()), + } + } + + if let Some((Token::Function { id, num_args, name }, _)) = + self.operator_stack.last() + { + let got_args = self.arg_count.pop().unwrap(); + if got_args != *num_args as i32 { + return Err(if *id != u32::MAX { + format!( + "Expression function {:?} expected {} arguments, got {}", + name, num_args, got_args + ) + } else { + "Missing array index".to_string() + }); + } + + let expr = match *id { + ID_ARRAY_ACCESS => Expression::ArrayAccess, + ID_ARRAY_BUILD => Expression::ArrayBuild(*num_args), + id => Expression::Function { + id, + num_args: *num_args, + }, + }; + + self.operator_stack.pop(); + self.output.push(expr); + } + + is_var_or_fnc = true; + } + Token::BinaryOperator(bop) => { + self.dec_arg_count(); + while let Some((top_token, prev_jmp_pos)) = self.operator_stack.last() { + match top_token { + Token::BinaryOperator(top_bop) => { + if bop.precedence() <= top_bop.precedence() { + let top_bop = *top_bop; + let jmp_pos = *prev_jmp_pos; + self.update_jmp_pos(jmp_pos); + self.operator_stack.pop(); + self.output.push(Expression::BinaryOperator(top_bop)); + } else { + break; + } + } + Token::UnaryOperator(top_uop) => { + let top_uop = *top_uop; + self.operator_stack.pop(); + self.output.push(Expression::UnaryOperator(top_uop)); + } + _ => break, + } + } + + // Add jump instruction for short-circuiting + let jmp_pos = match bop { + BinaryOperator::And => { + self.output.push(Expression::JmpIf { val: false, pos: 0 }); + Some(self.output.len() - 1) + } + BinaryOperator::Or => { + self.output.push(Expression::JmpIf { val: true, pos: 0 }); + Some(self.output.len() - 1) + } + _ => None, + }; + + self.operator_stack + .push((Token::BinaryOperator(bop), jmp_pos)); + } + Token::Function { id, name, num_args } => { + self.inc_arg_count(); + self.arg_count.push(0); + self.operator_stack + .push((Token::Function { id, name, num_args }, None)) + } + Token::OpenBracket => { + // Array functions + let (id, num_args, arg_count) = if last_is_var_or_fnc { + (ID_ARRAY_ACCESS, 2, 1) + } else { + self.inc_arg_count(); + (ID_ARRAY_BUILD, 0, 0) + }; + self.arg_count.push(arg_count); + self.operator_stack.push(( + Token::Function { + id, + name: String::from("array"), + num_args, + }, + None, + )); + self.operator_stack.push((token, None)); + } + Token::Comma => { + while let Some((token, jmp_pos)) = self.operator_stack.last() { + match token { + Token::OpenParen => break, + Token::BinaryOperator(bop) => { + let bop = *bop; + let jmp_pos = *jmp_pos; + self.update_jmp_pos(jmp_pos); + self.output.push(Expression::BinaryOperator(bop)); + self.operator_stack.pop(); + } + Token::UnaryOperator(uop) => { + self.output.push(Expression::UnaryOperator(*uop)); + self.operator_stack.pop(); + } + _ => break, + } + } + } + } + last_is_var_or_fnc = is_var_or_fnc; + } + + while let Some((token, jmp_pos)) = self.operator_stack.pop() { + match token { + Token::BinaryOperator(bop) => { + self.update_jmp_pos(jmp_pos); + self.output.push(Expression::BinaryOperator(bop)) + } + Token::UnaryOperator(uop) => self.output.push(Expression::UnaryOperator(uop)), + _ => return Err("Invalid token on the operator stack".to_string()), + } + } + + Ok(self) + } + + fn inc_arg_count(&mut self) { + if let Some(x) = self.arg_count.last_mut() { + *x = x.saturating_add(1); + let op_pos = self.operator_stack.len().saturating_sub(2); + match self.operator_stack.get_mut(op_pos) { + Some((Token::Function { num_args, id, .. }, _)) if *id == ID_ARRAY_BUILD => { + *num_args += 1; + } + _ => {} + } + } + } + + fn dec_arg_count(&mut self) { + if let Some(x) = self.arg_count.last_mut() { + *x = x.saturating_sub(1); + } + } + + fn update_jmp_pos(&mut self, jmp_pos: Option) { + if let Some(jmp_pos) = jmp_pos { + let cur_pos = self.output.len(); + if let Expression::JmpIf { pos, .. } = &mut self.output[jmp_pos] { + *pos = (cur_pos - jmp_pos) as u32; + } else { + #[cfg(test)] + panic!("Invalid jump position"); + } + } + } +} + +impl BinaryOperator { + fn precedence(&self) -> i32 { + match self { + BinaryOperator::Multiply | BinaryOperator::Divide => 7, + BinaryOperator::Add | BinaryOperator::Subtract => 6, + BinaryOperator::Gt | BinaryOperator::Ge | BinaryOperator::Lt | BinaryOperator::Le => 5, + BinaryOperator::Eq | BinaryOperator::Ne => 4, + BinaryOperator::Xor => 3, + BinaryOperator::And => 2, + BinaryOperator::Or => 1, + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/expr/tokenizer.rs b/vendor/sieve-rs/src/compiler/grammar/expr/tokenizer.rs new file mode 100644 index 0000000..40e94ea --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/expr/tokenizer.rs @@ -0,0 +1,285 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{ + iter::{Enumerate, Peekable}, + slice::Iter, +}; + +use crate::{compiler::Number, runtime::eval::IntoString}; + +use super::{BinaryOperator, Token, UnaryOperator}; + +pub(crate) struct Tokenizer<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + pub(crate) iter: Peekable>>, + token_map: F, + buf: Vec, + depth: u32, + next_token: Vec, + has_number: bool, + has_dot: bool, + has_alpha: bool, + is_start: bool, + is_eof: bool, +} + +impl<'x, F> Tokenizer<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + #[cfg(test)] + pub fn new(expr: &'x str, token_map: F) -> Self { + Self::from_iter(expr.as_bytes().iter().enumerate().peekable(), token_map) + } + + #[allow(clippy::should_implement_trait)] + pub(crate) fn from_iter(iter: Peekable>>, token_map: F) -> Self { + Self { + iter, + buf: Vec::new(), + depth: 0, + next_token: Vec::with_capacity(2), + has_number: false, + has_dot: false, + has_alpha: false, + is_start: true, + is_eof: false, + token_map, + } + } + + #[allow(clippy::should_implement_trait)] + pub(crate) fn next(&mut self) -> Result, String> { + if let Some(token) = self.next_token.pop() { + return Ok(Some(token)); + } else if self.is_eof { + return Ok(None); + } + + while let Some((_, &ch)) = self.iter.next() { + match ch { + b'A'..=b'Z' | b'a'..=b'z' | b'_' => { + self.buf.push(ch); + self.has_alpha = true; + } + b'0'..=b'9' => { + self.buf.push(ch); + self.has_number = true; + } + b'.' => { + self.buf.push(ch); + self.has_dot = true; + } + b'}' => { + self.is_eof = true; + break; + } + b'[' if matches!(self.buf.get(0..7), Some(b"header.")) => { + self.buf.push(ch); + } + b'-' if self.buf.last().is_some_and(|c| *c == b'[') + || matches!(self.buf.get(0..7), Some(b"header.")) => + { + self.buf.push(ch); + } + b':' if self.buf.contains(&b'.') => { + self.buf.push(ch); + } + b']' if self.buf.contains(&b'[') => { + self.buf.push(b']'); + } + b'*' if self.buf.last().is_some_and(|&c| c == b'[' || c == b'.') => { + self.buf.push(ch); + } + _ => { + let prev_token = if !self.buf.is_empty() { + self.is_start = false; + self.parse_buf()?.into() + } else { + None + }; + let token = match ch { + b'&' => { + if matches!(self.iter.peek(), Some((_, b'&'))) { + self.iter.next(); + } + Token::BinaryOperator(BinaryOperator::And) + } + b'|' => { + if matches!(self.iter.peek(), Some((_, b'|'))) { + self.iter.next(); + } + Token::BinaryOperator(BinaryOperator::Or) + } + b'!' => { + if matches!(self.iter.peek(), Some((_, b'='))) { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Ne) + } else { + Token::UnaryOperator(UnaryOperator::Not) + } + } + b'^' => Token::BinaryOperator(BinaryOperator::Xor), + b'(' => { + self.depth += 1; + Token::OpenParen + } + b')' => { + if self.depth == 0 { + return Err("Unmatched close parenthesis".to_string()); + } + self.depth -= 1; + Token::CloseParen + } + b'+' => Token::BinaryOperator(BinaryOperator::Add), + b'*' => Token::BinaryOperator(BinaryOperator::Multiply), + b'/' => Token::BinaryOperator(BinaryOperator::Divide), + b'-' => { + if self.is_start { + Token::UnaryOperator(UnaryOperator::Minus) + } else { + Token::BinaryOperator(BinaryOperator::Subtract) + } + } + b'=' => match self.iter.next() { + Some((_, b'=')) => Token::BinaryOperator(BinaryOperator::Eq), + Some((_, b'>')) => Token::BinaryOperator(BinaryOperator::Ge), + Some((_, b'<')) => Token::BinaryOperator(BinaryOperator::Le), + _ => Token::BinaryOperator(BinaryOperator::Eq), + }, + b'>' => match self.iter.peek() { + Some((_, b'=')) => { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Ge) + } + _ => Token::BinaryOperator(BinaryOperator::Gt), + }, + b'<' => match self.iter.peek() { + Some((_, b'=')) => { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Le) + } + _ => Token::BinaryOperator(BinaryOperator::Lt), + }, + b',' => Token::Comma, + b'[' => Token::OpenBracket, + b']' => Token::CloseBracket, + b' ' | b'\r' | b'\n' => { + if prev_token.is_some() { + return Ok(prev_token); + } else { + continue; + } + } + b'\"' | b'\'' => { + let mut buf = Vec::with_capacity(16); + let stop_ch = ch; + let mut last_ch = 0; + let mut found_end = false; + + for (_, &ch) in self.iter.by_ref() { + if last_ch != b'\\' { + if ch != stop_ch { + buf.push(ch); + } else { + found_end = true; + break; + } + } else { + match ch { + b'n' => { + buf.push(b'\n'); + } + b'r' => { + buf.push(b'\r'); + } + b't' => { + buf.push(b'\t'); + } + _ => { + buf.push(ch); + } + } + } + + last_ch = ch; + } + + if found_end { + Token::String( + String::from_utf8(buf) + .map_err(|_| "Invalid UTF-8".to_string())?, + ) + } else { + return Err("Unterminated string".to_string()); + } + } + _ => { + return Err(format!("Invalid character {:?}", char::from(ch),)); + } + }; + self.is_start = matches!( + token, + Token::OpenParen | Token::Comma | Token::BinaryOperator(_) + ); + + return if prev_token.is_some() { + self.next_token.push(token); + Ok(prev_token) + } else { + Ok(Some(token)) + }; + } + } + } + + if self.depth > 0 { + Err("Unmatched open parenthesis".to_string()) + } else if !self.buf.is_empty() { + self.parse_buf().map(Some) + } else { + Ok(None) + } + } + + fn parse_buf(&mut self) -> Result { + let buf = std::mem::take(&mut self.buf).into_string(); + if self.has_number && !self.has_alpha { + self.has_number = false; + if self.has_dot { + self.has_dot = false; + + buf.parse::() + .map(|f| Token::Number(Number::Float(f))) + .map_err(|_| format!("Invalid float value {}", buf,)) + } else { + buf.parse::() + .map(|i| Token::Number(Number::Integer(i))) + .map_err(|_| format!("Invalid integer value {}", buf,)) + } + } else { + let has_dot = self.has_dot; + let has_number = self.has_number; + + self.has_alpha = false; + self.has_number = false; + self.has_dot = false; + + if !has_number && !has_dot && [4, 5].contains(&buf.len()) { + if buf == "true" { + return Ok(Token::Number(Number::Integer(1))); + } else if buf == "false" { + return Ok(Token::Number(Number::Integer(0))); + } + } + + (self.token_map)(&buf, has_dot) + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/instruction.rs b/vendor/sieve-rs/src/compiler/grammar/instruction.rs new file mode 100644 index 0000000..76929fd --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/instruction.rs @@ -0,0 +1,1221 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use ahash::{AHashMap, AHashSet}; + +use crate::{ + Compiler, Sieve, + compiler::{ + CompileError, ErrorType, Value, VariableType, + grammar::{MatchType, test::Test}, + lexer::{Token, tokenizer::Tokenizer, word::Word}, + }, +}; + +use super::{ + Capability, Clear, Invalid, While, + actions::{ + action_convert::Convert, + action_editheader::{AddHeader, DeleteHeader}, + action_fileinto::FileInto, + action_flags::EditFlags, + action_include::Include, + action_keep::Keep, + action_mime::{Enclose, ExtractText, ForEveryPart, Replace}, + action_notify::Notify, + action_redirect::Redirect, + action_reject::Reject, + action_set::{Let, Set}, + action_vacation::Vacation, + }, + expr::Expression, +}; + +use super::tests::test_ihave::Error; + +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Instruction { + Require(Vec), + Keep(Keep), + FileInto(FileInto), + Redirect(Redirect), + Discard, + Stop, + Invalid(Invalid), + Test(Test), + Jmp(usize), + Jz(usize), + Jnz(usize), + + // RFC 5703 + ForEveryPartPush, + ForEveryPart(ForEveryPart), + ForEveryPartPop(usize), + Replace(Replace), + Enclose(Enclose), + ExtractText(ExtractText), + + // RFC 6558 + Convert(Convert), + + // RFC 5293 + AddHeader(AddHeader), + DeleteHeader(DeleteHeader), + + // RFC 5229 + Set(Set), + Clear(Clear), + + // RFC 5435 + Notify(Notify), + + // RFC 5429 + Reject(Reject), + + // RFC 5230 + Vacation(Vacation), + + // RFC 5463 + Error(Error), + + // RFC 5232 + EditFlags(EditFlags), + + // RFC 6609 + Include(Include), + Return, + + // For every line extension + While(While), + + // Expression extension + Eval(Vec), + Let(Let), + + // Test only + #[cfg(test)] + TestCmd(Vec), +} + +pub(crate) const MAX_PARAMS: usize = 11; + +#[derive(Debug)] +pub(crate) struct Block { + pub(crate) btype: Word, + pub(crate) label: Option, + pub(crate) line_num: usize, + pub(crate) line_pos: usize, + pub(crate) last_block_start: usize, + pub(crate) if_jmps: Vec, + pub(crate) break_jmps: Vec, + pub(crate) match_test_pos: Vec, + pub(crate) match_test_vars: u64, + pub(crate) vars_local: AHashMap, + pub(crate) capabilities: AHashSet, + pub(crate) require_pos: usize, +} + +pub(crate) struct CompilerState<'x> { + pub(crate) compiler: &'x Compiler, + pub(crate) tokens: Tokenizer<'x>, + pub(crate) instructions: Vec, + pub(crate) block_stack: Vec, + pub(crate) block: Block, + pub(crate) last_block_type: Word, + pub(crate) vars_global: AHashSet, + pub(crate) vars_num: usize, + pub(crate) vars_num_max: usize, + pub(crate) vars_match_max: usize, + pub(crate) vars_local: usize, + pub(crate) param_check: [bool; MAX_PARAMS], + pub(crate) includes_num: usize, +} + +impl Compiler { + pub fn compile(&self, script: &[u8]) -> Result { + if script.len() > self.max_script_size { + return Err(CompileError { + line_num: 0, + line_pos: 0, + error_type: ErrorType::ScriptTooLong, + }); + } + + let mut state = CompilerState { + compiler: self, + tokens: Tokenizer::new(self, script), + instructions: Vec::new(), + block_stack: Vec::new(), + block: Block::new(Word::Not), + last_block_type: Word::Not, + vars_global: AHashSet::new(), + vars_num: 0, + vars_num_max: 0, + vars_match_max: 0, + vars_local: 0, + param_check: [false; MAX_PARAMS], + includes_num: 0, + }; + + while let Some(token_info) = state.tokens.next() { + let token_info = token_info?; + state.reset_param_check(); + + match token_info.token { + Token::Identifier(instruction) => { + let mut is_new_block = None; + + match instruction { + Word::Require => { + state.parse_require()?; + } + Word::If => { + state.parse_test()?; + state.block.if_jmps.clear(); + is_new_block = Block::new(Word::If).into(); + } + Word::ElsIf => { + if let Word::If | Word::ElsIf = &state.last_block_type { + state.parse_test()?; + is_new_block = Block::new(Word::ElsIf).into(); + } else { + return Err(token_info.expected("'if' before 'elsif'")); + } + } + Word::Else => { + if let Word::If | Word::ElsIf = &state.last_block_type { + is_new_block = Block::new(Word::Else).into(); + } else { + return Err(token_info.expected("'if' or 'elsif' before 'else'")); + } + } + Word::Keep => { + state.parse_keep()?; + } + Word::FileInto => { + state.validate_argument( + 0, + Capability::FileInto.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_fileinto()?; + } + Word::Redirect => { + state.parse_redirect()?; + } + Word::Discard => { + state.instructions.push(Instruction::Discard); + } + Word::Stop => { + state.instructions.push(Instruction::Stop); + } + + // RFC 5703 + Word::ForEveryPart => { + state.validate_argument( + 0, + Capability::ForEveryPart.into(), + token_info.line_num, + token_info.line_pos, + )?; + + if state + .block_stack + .iter() + .filter(|b| matches!(&b.btype, Word::ForEveryPart)) + .count() + == self.max_nested_foreverypart + { + return Err( + token_info.custom(ErrorType::TooManyNestedForEveryParts) + ); + } + + is_new_block = if let Some(Ok(Token::Tag(Word::Name))) = + state.tokens.peek().map(|r| r.map(|t| &t.token)) + { + let tag = state.tokens.next().unwrap().unwrap(); + let label = state.tokens.expect_static_string()?; + for block in &state.block_stack { + if block.label.as_ref().is_some_and(|n| n.eq(&label)) { + return Err( + tag.custom(ErrorType::LabelAlreadyDefined(label)) + ); + } + } + Block::new(Word::ForEveryPart).with_label(label) + } else { + Block::new(Word::ForEveryPart) + } + .into(); + + state.instructions.push(Instruction::ForEveryPartPush); + state + .instructions + .push(Instruction::ForEveryPart(ForEveryPart { + jz_pos: usize::MAX, + })); + } + Word::Break => { + if let Some(Ok(Token::Tag(Word::Name))) = + state.tokens.peek().map(|r| r.map(|t| &t.token)) + { + state.validate_argument( + 0, + Capability::ForEveryPart.into(), + token_info.line_num, + token_info.line_pos, + )?; + + let tag = state.tokens.next().unwrap().unwrap(); + let label = state.tokens.expect_static_string()?; + let mut label_found = false; + let mut num_pops = 0; + + for block in [&mut state.block] + .into_iter() + .chain(state.block_stack.iter_mut().rev()) + { + if let Word::ForEveryPart = &block.btype { + num_pops += 1; + if block.label.as_ref().is_some_and(|n| n.eq(&label)) { + state + .instructions + .push(Instruction::ForEveryPartPop(num_pops)); + block.break_jmps.push(state.instructions.len()); + label_found = true; + break; + } + } + } + + if !label_found { + return Err(tag.custom(ErrorType::LabelUndefined(label))); + } + } else { + let mut block_found = None; + if matches!(&state.block.btype, Word::ForEveryPart | Word::While) { + block_found = Some(&mut state.block); + } else { + for block in state.block_stack.iter_mut().rev() { + if matches!(&block.btype, Word::ForEveryPart | Word::While) + { + block_found = Some(block); + break; + } + } + } + + let block = block_found.ok_or_else(|| { + token_info.custom(ErrorType::BreakOutsideLoop) + })?; + if matches!(block.btype, Word::ForEveryPart) { + state.instructions.push(Instruction::ForEveryPartPop(1)); + } + + block.break_jmps.push(state.instructions.len()); + } + + state.instructions.push(Instruction::Jmp(usize::MAX)); + } + Word::Replace => { + state.validate_argument( + 0, + Capability::Replace.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_replace()?; + } + Word::Enclose => { + state.validate_argument( + 0, + Capability::Enclose.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_enclose()?; + } + Word::ExtractText => { + state.validate_argument( + 0, + Capability::ExtractText.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_extracttext()?; + } + + // RFC 6558 + Word::Convert => { + state.validate_argument( + 0, + Capability::Convert.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_convert()?; + } + + // RFC 5293 + Word::AddHeader => { + state.validate_argument( + 0, + Capability::EditHeader.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_addheader()?; + } + Word::DeleteHeader => { + state.validate_argument( + 0, + Capability::EditHeader.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_deleteheader()?; + } + + // RFC 5229 + Word::Set => { + state.validate_argument( + 0, + Capability::Variables.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_set()?; + } + + // RFC 5435 + Word::Notify => { + state.validate_argument( + 0, + Capability::Enotify.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_notify()?; + } + + // RFC 5429 + Word::Reject => { + state.validate_argument( + 0, + Capability::Reject.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_reject(false)?; + } + Word::Ereject => { + state.validate_argument( + 0, + Capability::Ereject.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_reject(true)?; + } + + // RFC 5230 + Word::Vacation => { + state.validate_argument( + 0, + Capability::Vacation.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_vacation()?; + } + + // RFC 5463 + Word::Error => { + state.validate_argument( + 0, + Capability::Ihave.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_error()?; + } + + // RFC 5232 + Word::SetFlag | Word::AddFlag | Word::RemoveFlag => { + state.validate_argument( + 0, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_flag_action(instruction)?; + } + + // RFC 6609 + Word::Include => { + if state.includes_num < self.max_includes { + state.validate_argument( + 0, + Capability::Include.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_include()?; + state.includes_num += 1; + } else { + return Err(token_info.custom(ErrorType::TooManyIncludes)); + } + } + Word::Return => { + state.validate_argument( + 0, + Capability::Include.into(), + token_info.line_num, + token_info.line_pos, + )?; + let mut num_pops = 0; + + for block in [&state.block] + .into_iter() + .chain(state.block_stack.iter().rev()) + { + if let Word::ForEveryPart = &block.btype { + num_pops += 1; + } + } + + if num_pops > 0 { + state + .instructions + .push(Instruction::ForEveryPartPop(num_pops)); + } + + state.instructions.push(Instruction::Return); + } + Word::Global => { + state.validate_argument( + 0, + Capability::Include.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.validate_argument( + 0, + Capability::Variables.into(), + token_info.line_num, + token_info.line_pos, + )?; + for global in state.parse_static_strings()? { + if !state.is_var_local(&global) { + if global.len() < self.max_variable_name_size { + state.register_global_var(&global); + } else { + return Err(state + .tokens + .unwrap_next()? + .custom(ErrorType::VariableTooLong)); + } + } else { + return Err(state + .tokens + .unwrap_next()? + .custom(ErrorType::VariableIsLocal(global))); + } + } + } + + // Expressions extension + Word::Let => { + state.validate_argument( + 0, + Capability::Expressions.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_let()?; + } + Word::Eval => { + state.validate_argument( + 0, + Capability::Expressions.into(), + token_info.line_num, + token_info.line_pos, + )?; + let expr = state.parse_expr()?; + state.instructions.push(Instruction::Eval(expr)); + } + + // While extension + Word::While => { + state.validate_argument( + 0, + Capability::While.into(), + token_info.line_num, + token_info.line_pos, + )?; + + is_new_block = Block::new(Word::While).into(); + + let expr = state.parse_expr()?; + state.instructions.push(Instruction::While(While { + expr, + jz_pos: usize::MAX, + })); + } + Word::Continue => { + state.validate_argument( + 0, + Capability::While.into(), + token_info.line_num, + token_info.line_pos, + )?; + let mut found_while = 0; + for block in [&state.block] + .into_iter() + .chain(state.block_stack.iter().rev()) + { + if let Word::While = &block.btype { + found_while += 1; + } else if found_while == 1 { + state + .instructions + .push(Instruction::Jmp(block.last_block_start)); + found_while += 1; + break; + } + } + if found_while != 2 { + return Err(token_info.custom(ErrorType::ContinueOutsideLoop)); + } + } + + _ => { + if state.has_capability(&Capability::Ihave) { + state.ignore_instruction()?; + state.instructions.push(Instruction::Invalid(Invalid { + name: instruction.to_string(), + line_num: token_info.line_num, + line_pos: token_info.line_pos, + })); + continue; + } else { + return Err(CompileError { + line_num: state.block.line_num, + line_pos: state.block.line_pos, + error_type: ErrorType::UnexpectedToken { + expected: "command".into(), + found: instruction.to_string(), + }, + }); + } + } + } + + if let Some(mut new_block) = is_new_block { + new_block.line_num = state.tokens.line_num; + new_block.line_pos = state.tokens.pos - state.tokens.line_start; + + state.tokens.expect_token(Token::CurlyOpen)?; + if state.block_stack.len() < self.max_nested_blocks { + state.block.last_block_start = state.instructions.len() - 1; + state.block_stack.push(state.block); + state.block = new_block; + } else { + return Err(CompileError { + line_num: state.block.line_num, + line_pos: state.block.line_pos, + error_type: ErrorType::TooManyNestedBlocks, + }); + } + } else { + state.expect_instruction_end()?; + } + } + Token::CurlyClose if !state.block_stack.is_empty() => { + state.block_end(); + let mut prev_block = state.block_stack.pop().unwrap(); + match &state.block.btype { + Word::ForEveryPart => { + state + .instructions + .push(Instruction::Jmp(prev_block.last_block_start)); + let cur_pos = state.instructions.len(); + if let Instruction::ForEveryPart(fep) = + &mut state.instructions[prev_block.last_block_start] + { + fep.jz_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + for pos in state.block.break_jmps { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Not; + } + Word::If | Word::ElsIf => { + let next_is_block = matches!( + state.tokens.peek().map(|r| r.map(|t| &t.token)), + Some(Ok(Token::Identifier(Word::ElsIf | Word::Else))) + ); + if next_is_block { + prev_block.if_jmps.push(state.instructions.len()); + state.instructions.push(Instruction::Jmp(usize::MAX)); + } + let cur_pos = state.instructions.len(); + if let Instruction::Jz(jmp_pos) = + &mut state.instructions[prev_block.last_block_start] + { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + if !next_is_block { + for pos in prev_block.if_jmps.drain(..) { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] + { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Not; + } else { + state.last_block_type = state.block.btype; + } + } + Word::Else => { + let cur_pos = state.instructions.len(); + for pos in prev_block.if_jmps.drain(..) { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Else; + } + Word::While => { + state + .instructions + .push(Instruction::Jmp(prev_block.last_block_start)); + let cur_pos = state.instructions.len(); + if let Instruction::While(fep) = + &mut state.instructions[prev_block.last_block_start] + { + fep.jz_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + for pos in state.block.break_jmps { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Not; + } + _ => { + debug_assert!(false, "This should not have happened."); + } + } + + state.block = prev_block; + } + + #[cfg(test)] + Token::Unknown(instruction) if instruction.contains("test") => { + let has_arguments = instruction != "test"; + let mut arguments = vec![Value::Text(instruction.into())]; + + if !has_arguments { + arguments.push(state.parse_string()?); + state.instructions.push(Instruction::TestCmd(arguments)); + let mut new_block = Block::new(Word::Else); + new_block.line_num = state.tokens.line_num; + new_block.line_pos = state.tokens.pos - state.tokens.line_start; + state.tokens.expect_token(Token::CurlyOpen)?; + state.block.last_block_start = state.instructions.len() - 1; + state.block_stack.push(state.block); + state.block = new_block; + } else { + loop { + arguments.push(match state.tokens.unwrap_next()?.token { + Token::StringConstant(s) => Value::from(s), + Token::StringVariable(s) => state + .tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: 0, + line_pos: 0, + error_type, + })?, + Token::Number(n) => { + Value::Number(crate::compiler::Number::Integer(n as i64)) + } + Token::Identifier(s) => Value::Text(s.to_string().into()), + Token::Tag(s) => Value::Text(format!(":{s}").into()), + Token::Unknown(s) => Value::Text(s.into()), + Token::Semicolon => break, + other => panic!("Invalid test param {other:?}"), + }); + } + state.instructions.push(Instruction::TestCmd(arguments)); + } + } + + Token::Unknown(instruction) => { + if state.has_capability(&Capability::Ihave) { + state.ignore_instruction()?; + state.instructions.push(Instruction::Invalid(Invalid { + name: instruction, + line_num: token_info.line_num, + line_pos: token_info.line_pos, + })); + } else { + return Err(CompileError { + line_num: state.block.line_num, + line_pos: state.block.line_pos, + error_type: ErrorType::UnexpectedToken { + expected: "command".into(), + found: instruction, + }, + }); + } + } + _ => { + return Err(token_info.expected("instruction")); + } + } + } + + if !state.block_stack.is_empty() { + return Err(CompileError { + line_num: state.block.line_num, + line_pos: state.block.line_pos, + error_type: ErrorType::UnterminatedBlock, + }); + } + + // Map local variables + let mut num_vars = std::cmp::max(state.vars_num_max, state.vars_num); + if state.vars_local > 0 { + state.map_local_vars(num_vars); + num_vars += state.vars_local; + } + + Ok(Sieve { + instructions: state.instructions, + num_vars: num_vars as u32, + num_match_vars: state.vars_match_max as u32, + }) + } +} + +impl CompilerState<'_> { + pub(crate) fn is_var_local(&self, name: &str) -> bool { + let name = name.to_ascii_lowercase(); + if self.block.vars_local.contains_key(&name) { + true + } else { + for block in self.block_stack.iter().rev() { + if block.vars_local.contains_key(&name) { + return true; + } + } + false + } + } + + pub(crate) fn is_var_global(&self, name: &str) -> bool { + let name = name.to_ascii_lowercase(); + self.vars_global.contains(&name) + } + + pub(crate) fn register_local_var(&mut self, name: String, register_as_local: bool) -> usize { + if let Some(var_id) = self.get_local_var(&name) { + var_id + } else if !register_as_local || self.block_stack.is_empty() { + let var_id = self.vars_num; + self.block.vars_local.insert(name, var_id); + self.vars_num += 1; + var_id + } else { + let var_id = usize::MAX - self.vars_local; + self.block_stack + .first_mut() + .unwrap() + .vars_local + .insert(name, usize::MAX - self.vars_local); + self.vars_local += 1; + var_id + } + } + + pub(crate) fn register_global_var(&mut self, name: &str) { + self.vars_global.insert(name.to_ascii_lowercase()); + } + + pub(crate) fn get_local_var(&self, name: &str) -> Option { + let name = name.to_ascii_lowercase(); + if let Some(var_id) = self.block.vars_local.get(&name) { + Some(*var_id) + } else { + for block in self.block_stack.iter().rev() { + if let Some(var_id) = block.vars_local.get(&name) { + return Some(*var_id); + } + } + None + } + } + + pub(crate) fn register_match_var(&mut self, num: usize) -> bool { + let mut block = &mut self.block; + + if block.match_test_pos.is_empty() { + for block_ in self.block_stack.iter_mut().rev() { + if !block_.match_test_pos.is_empty() { + block = block_; + break; + } + } + } + + if !block.match_test_pos.is_empty() { + debug_assert!(num < 63); + + for pos in &block.match_test_pos { + if let Some(Instruction::Test(test)) = self.instructions.get_mut(*pos) { + let match_type = match test { + Test::Address(t) => &mut t.match_type, + Test::Body(t) => &mut t.match_type, + Test::Date(t) => &mut t.match_type, + Test::CurrentDate(t) => &mut t.match_type, + Test::Envelope(t) => &mut t.match_type, + Test::HasFlag(t) => &mut t.match_type, + Test::Header(t) => &mut t.match_type, + Test::Metadata(t) => &mut t.match_type, + Test::NotifyMethodCapability(t) => &mut t.match_type, + Test::SpamTest(t) => &mut t.match_type, + Test::String(t) | Test::Environment(t) => &mut t.match_type, + Test::VirusTest(t) => &mut t.match_type, + _ => { + debug_assert!(false, "This should not have happened: {test:?}"); + return false; + } + }; + if let MatchType::Matches(positions) | MatchType::Regex(positions) = match_type + { + *positions |= 1 << num; + block.match_test_vars = *positions; + } else { + debug_assert!(false, "This should not have happened"); + return false; + } + } else { + debug_assert!(false, "This should not have happened"); + return false; + } + } + true + } else { + false + } + } + + pub(crate) fn block_end(&mut self) { + let vars_num_block = self.block.vars_local.len(); + if vars_num_block > 0 { + if self.vars_num > self.vars_num_max { + self.vars_num_max = self.vars_num; + } + self.vars_num -= vars_num_block; + self.instructions.push(Instruction::Clear(Clear { + match_vars: self.block.match_test_vars, + local_vars_idx: self.vars_num as u32, + local_vars_num: vars_num_block as u32, + })); + } else if self.block.match_test_vars != 0 { + self.instructions.push(Instruction::Clear(Clear { + match_vars: self.block.match_test_vars, + local_vars_idx: 0, + local_vars_num: 0, + })); + } + } + + fn map_local_vars(&mut self, last_id: usize) { + for instruction in &mut self.instructions { + match instruction { + Instruction::Test(v) => v.map_local_vars(last_id), + Instruction::Keep(k) => k.flags.map_local_vars(last_id), + Instruction::FileInto(v) => { + v.folder.map_local_vars(last_id); + v.flags.map_local_vars(last_id); + v.mailbox_id.map_local_vars(last_id); + v.special_use.map_local_vars(last_id); + } + Instruction::Redirect(v) => { + v.address.map_local_vars(last_id); + v.by_time.map_local_vars(last_id); + } + Instruction::Replace(v) => { + v.subject.map_local_vars(last_id); + v.from.map_local_vars(last_id); + v.replacement.map_local_vars(last_id); + } + Instruction::Enclose(v) => { + v.subject.map_local_vars(last_id); + v.headers.map_local_vars(last_id); + v.value.map_local_vars(last_id); + } + Instruction::ExtractText(v) => { + v.name.map_local_vars(last_id); + } + Instruction::Convert(v) => { + v.from_media_type.map_local_vars(last_id); + v.to_media_type.map_local_vars(last_id); + v.transcoding_params.map_local_vars(last_id); + } + Instruction::AddHeader(v) => { + v.field_name.map_local_vars(last_id); + v.value.map_local_vars(last_id); + } + Instruction::DeleteHeader(v) => { + v.field_name.map_local_vars(last_id); + v.value_patterns.map_local_vars(last_id); + } + Instruction::Set(v) => { + v.name.map_local_vars(last_id); + v.value.map_local_vars(last_id); + } + Instruction::Let(v) => { + v.name.map_local_vars(last_id); + v.expr.map_local_vars(last_id); + } + Instruction::While(v) => { + v.expr.map_local_vars(last_id); + } + Instruction::Eval(v) => { + v.map_local_vars(last_id); + } + Instruction::Notify(v) => { + v.from.map_local_vars(last_id); + v.importance.map_local_vars(last_id); + v.options.map_local_vars(last_id); + v.message.map_local_vars(last_id); + v.fcc.map_local_vars(last_id); + v.method.map_local_vars(last_id); + } + Instruction::Reject(v) => { + v.reason.map_local_vars(last_id); + } + Instruction::Vacation(v) => { + v.subject.map_local_vars(last_id); + v.from.map_local_vars(last_id); + v.fcc.map_local_vars(last_id); + v.reason.map_local_vars(last_id); + } + Instruction::Error(v) => { + v.message.map_local_vars(last_id); + } + Instruction::EditFlags(v) => { + v.name.map_local_vars(last_id); + v.flags.map_local_vars(last_id); + } + Instruction::Include(v) => { + v.value.map_local_vars(last_id); + } + _ => {} + } + } + } +} + +pub trait MapLocalVars { + fn map_local_vars(&mut self, last_id: usize); +} + +impl MapLocalVars for Test { + fn map_local_vars(&mut self, last_id: usize) { + match self { + Test::Address(v) => { + v.header_list.map_local_vars(last_id); + v.key_list.map_local_vars(last_id); + } + Test::Envelope(v) => { + v.key_list.map_local_vars(last_id); + } + Test::Exists(v) => { + v.header_names.map_local_vars(last_id); + } + Test::Header(v) => { + v.key_list.map_local_vars(last_id); + v.header_list.map_local_vars(last_id); + v.mime_opts.map_local_vars(last_id); + } + Test::Body(v) => { + v.key_list.map_local_vars(last_id); + } + Test::Convert(v) => { + v.from_media_type.map_local_vars(last_id); + v.to_media_type.map_local_vars(last_id); + v.transcoding_params.map_local_vars(last_id); + } + Test::Date(v) => { + v.key_list.map_local_vars(last_id); + v.header_name.map_local_vars(last_id); + } + Test::CurrentDate(v) => { + v.key_list.map_local_vars(last_id); + } + Test::Duplicate(v) => { + v.handle.map_local_vars(last_id); + v.dup_match.map_local_vars(last_id); + } + Test::String(v) => { + v.source.map_local_vars(last_id); + v.key_list.map_local_vars(last_id); + } + Test::Environment(v) => { + v.source.map_local_vars(last_id); + v.key_list.map_local_vars(last_id); + } + Test::NotifyMethodCapability(v) => { + v.key_list.map_local_vars(last_id); + v.notification_capability.map_local_vars(last_id); + v.notification_uri.map_local_vars(last_id); + } + Test::ValidNotifyMethod(v) => { + v.notification_uris.map_local_vars(last_id); + } + Test::ValidExtList(v) => { + v.list_names.map_local_vars(last_id); + } + Test::HasFlag(v) => { + v.variable_list.map_local_vars(last_id); + v.flags.map_local_vars(last_id); + } + Test::MailboxExists(v) => { + v.mailbox_names.map_local_vars(last_id); + } + Test::Metadata(v) => { + v.key_list.map_local_vars(last_id); + v.medatata.map_local_vars(last_id); + } + Test::MetadataExists(v) => { + v.annotation_names.map_local_vars(last_id); + v.mailbox.map_local_vars(last_id); + } + Test::MailboxIdExists(v) => { + v.mailbox_ids.map_local_vars(last_id); + } + Test::SpamTest(v) => { + v.value.map_local_vars(last_id); + } + Test::VirusTest(v) => { + v.value.map_local_vars(last_id); + } + Test::SpecialUseExists(v) => { + v.mailbox.map_local_vars(last_id); + v.attributes.map_local_vars(last_id); + } + Test::Vacation(v) => { + v.addresses.map_local_vars(last_id); + v.handle.map_local_vars(last_id); + v.reason.map_local_vars(last_id); + } + #[cfg(test)] + Test::TestCmd { arguments, .. } => { + arguments.map_local_vars(last_id); + } + _ => (), + } + } +} + +impl MapLocalVars for VariableType { + fn map_local_vars(&mut self, last_id: usize) { + match self { + VariableType::Local(id) if *id > last_id => { + *id = (usize::MAX - *id) + last_id; + } + _ => (), + } + } +} + +impl MapLocalVars for Value { + fn map_local_vars(&mut self, last_id: usize) { + match self { + Value::Variable(var) => var.map_local_vars(last_id), + Value::List(items) => items.map_local_vars(last_id), + _ => (), + } + } +} + +impl MapLocalVars for Option { + fn map_local_vars(&mut self, last_id: usize) { + if let Some(value) = self { + value.map_local_vars(last_id); + } + } +} + +impl MapLocalVars for Expression { + fn map_local_vars(&mut self, last_id: usize) { + if let Expression::Variable(var) = self { + var.map_local_vars(last_id) + } + } +} + +impl MapLocalVars for Vec { + fn map_local_vars(&mut self, last_id: usize) { + for item in self { + item.map_local_vars(last_id); + } + } +} + +impl Block { + pub fn new(btype: Word) -> Self { + Block { + btype, + label: None, + line_num: 0, + line_pos: 0, + last_block_start: 0, + match_test_pos: vec![], + match_test_vars: 0, + if_jmps: vec![], + break_jmps: vec![], + vars_local: AHashMap::new(), + capabilities: AHashSet::new(), + require_pos: usize::MAX, + } + } + + pub fn with_label(mut self, label: String) -> Self { + self.label = label.into(); + self + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/mod.rs b/vendor/sieve-rs/src/compiler/grammar/mod.rs new file mode 100644 index 0000000..59a9f90 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/mod.rs @@ -0,0 +1,736 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. + */ + +use std::fmt::Display; + +use self::{expr::Expression, instruction::CompilerState}; + +use super::{ + CompileError, ErrorType, Regex, Value, + lexer::{Token, tokenizer::TokenInfo, word::Word}, +}; + +pub mod actions; +pub mod expr; +pub mod instruction; +pub mod test; +pub mod tests; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Capability { + Envelope, + EnvelopeDsn, + EnvelopeDeliverBy, + FileInto, + EncodedCharacter, + Comparator(Comparator), + Other(String), + Body, + Convert, + Copy, + Relational, + Date, + Index, + Duplicate, + Variables, + EditHeader, + ForEveryPart, + Mime, + Replace, + Enclose, + ExtractText, + Enotify, + RedirectDsn, + RedirectDeliverBy, + Environment, + Reject, + Ereject, + ExtLists, + SubAddress, + Vacation, + VacationSeconds, + Fcc, + Mailbox, + MailboxId, + MboxMetadata, + ServerMetadata, + SpecialUse, + Imap4Flags, + Ihave, + ImapSieve, + Include, + Regex, + SpamTest, + SpamTestPlus, + VirusTest, + + // Extensions + Expressions, + While, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum AddressPart { + LocalPart, + Domain, + All, + User, + Detail, + Name, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum MatchType { + Is, + Contains, + Matches(u64), + Regex(u64), + Value(RelationalMatch), + Count(RelationalMatch), + List, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum RelationalMatch { + Gt, + Ge, + Lt, + Le, + Eq, + Ne, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Comparator { + Elbonia, + Octet, + AsciiCaseMap, + AsciiNumeric, + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub struct Clear { + pub(crate) local_vars_idx: u32, + pub(crate) local_vars_num: u32, + pub(crate) match_vars: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub struct Invalid { + pub(crate) name: String, + pub(crate) line_num: usize, + pub(crate) line_pos: usize, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct While { + pub expr: Vec, + pub jz_pos: usize, +} + +impl CompilerState<'_> { + #[inline(always)] + pub fn expect_instruction_end(&mut self) -> Result<(), CompileError> { + self.tokens.expect_token(Token::Semicolon) + } + + pub fn ignore_instruction(&mut self) -> Result<(), CompileError> { + // Skip entire instruction + let mut curly_count = 0; + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Semicolon if curly_count == 0 => { + break; + } + Token::CurlyOpen => { + curly_count += 1; + } + Token::CurlyClose => match curly_count { + 0 => { + return Err(token_info.expected("instruction")); + } + 1 => { + break; + } + _ => curly_count -= 1, + }, + _ => (), + } + } + + Ok(()) + } + + pub fn ignore_test(&mut self) -> Result<(), CompileError> { + let mut d_count = 0; + while let Some(token_info) = self.tokens.peek() { + match token_info?.token { + Token::ParenthesisOpen => { + d_count += 1; + } + Token::ParenthesisClose => { + if d_count == 0 { + break; + } else { + d_count -= 1; + } + } + Token::Comma if d_count == 0 => { + break; + } + Token::CurlyOpen => { + break; + } + _ => (), + } + self.tokens.next(); + } + + Ok(()) + } + + pub fn parse_match_type(&mut self, word: Word) -> Result { + match word { + Word::Is => Ok(MatchType::Is), + Word::Contains => Ok(MatchType::Contains), + Word::Matches => { + self.block.match_test_pos.push(self.instructions.len()); + Ok(MatchType::Matches(0)) + } + Word::Regex => { + self.block.match_test_pos.push(self.instructions.len()); + Ok(MatchType::Regex(0)) + } + Word::List => Ok(MatchType::List), + _ => { + let token_info = self.tokens.unwrap_next()?; + if let Token::StringConstant(text) = &token_info.token + && let Some(relational) = lookup_relational(text.to_string().as_ref()) + { + return Ok(if word == Word::Value { + MatchType::Value(relational) + } else { + MatchType::Count(relational) + }); + } + Err(token_info.expected("relational match")) + } + } + } + + pub(crate) fn parse_comparator(&mut self) -> Result { + let comparator = self.tokens.expect_static_string()?; + Ok(if let Some(comparator) = lookup_comparator(&comparator) { + comparator + } else { + Comparator::Other(comparator) + }) + } + + pub(crate) fn parse_static_strings(&mut self) -> Result, CompileError> { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::BracketOpen => { + let mut strings = Vec::new(); + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(string) => { + strings.push(string.into_string()); + } + Token::Comma => (), + Token::BracketClose if !strings.is_empty() => break, + _ => return Err(token_info.expected("constant string")), + } + } + Ok(strings) + } + Token::StringConstant(string) => Ok(vec![string.into_string()]), + _ => Err(token_info.expected("'[' or constant string")), + } + } + + pub fn parse_string(&mut self) -> Result { + let next_token = self.tokens.unwrap_next()?; + match next_token.token { + Token::StringConstant(s) => Ok(Value::from(s)), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: next_token.line_num, + line_pos: next_token.line_pos, + error_type, + }) + } + Token::BracketOpen => { + let mut items = self.parse_string_list(false)?; + match items.pop() { + Some(s) if items.is_empty() => Ok(s), + _ => Err(next_token.expected("string")), + } + } + _ => Err(next_token.expected("string")), + } + } + + pub(crate) fn parse_strings(&mut self, allow_empty: bool) -> Result, CompileError> { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::BracketOpen => self.parse_string_list(allow_empty), + Token::StringConstant(s) => Ok(vec![Value::from(s)]), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map(|s| vec![s]) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }) + } + _ => Err(token_info.expected("'[' or string")), + } + } + + pub(crate) fn parse_string_token( + &mut self, + token_info: TokenInfo, + ) -> Result { + match token_info.token { + Token::StringConstant(s) => Ok(Value::from(s)), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }) + } + _ => Err(token_info.expected("string")), + } + } + + pub(crate) fn parse_strings_token( + &mut self, + token_info: TokenInfo, + ) -> Result, CompileError> { + match token_info.token { + Token::StringConstant(s) => Ok(vec![Value::from(s)]), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map(|s| vec![s]) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }) + } + Token::BracketOpen => self.parse_string_list(false), + _ => Err(token_info.expected("string")), + } + } + + pub(crate) fn parse_string_list( + &mut self, + allow_empty: bool, + ) -> Result, CompileError> { + let mut strings = Vec::new(); + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(s) => { + strings.push(Value::from(s)); + } + Token::StringVariable(s) => { + strings.push(self.tokenize_string(&s, true).map_err(|error_type| { + CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + } + })?); + } + Token::Comma => (), + Token::BracketClose if !strings.is_empty() || allow_empty => break, + _ => return Err(token_info.expected("string or string list")), + } + } + Ok(strings) + } + + #[inline(always)] + pub(crate) fn has_capability(&self, capability: &Capability) -> bool { + [&self.block] + .into_iter() + .chain(self.block_stack.iter()) + .any(|b| b.capabilities.contains(capability)) + || (capability != &Capability::Ihave && self.compiler.no_capability_check) + } + + #[inline(always)] + pub(crate) fn reset_param_check(&mut self) { + self.param_check.fill(false); + } + + #[inline(always)] + pub(crate) fn validate_argument( + &mut self, + arg_num: usize, + capability: Option, + line_num: usize, + line_pos: usize, + ) -> Result<(), CompileError> { + if arg_num > 0 { + if let Some(param) = self.param_check.get_mut(arg_num - 1) { + if !*param { + *param = true; + } else { + return Err(CompileError { + line_num, + line_pos, + error_type: ErrorType::DuplicatedParameter, + }); + } + } else { + #[cfg(test)] + panic!("Argument out of range {arg_num}"); + } + } + if let Some(capability) = capability + && !self.has_capability(&capability) + { + return Err(CompileError { + line_num, + line_pos, + error_type: ErrorType::UndeclaredCapability(capability), + }); + } + + Ok(()) + } + + pub(crate) fn validate_match( + &mut self, + match_type: &MatchType, + key_list: &mut [Value], + ) -> Result<(), CompileError> { + if matches!(match_type, MatchType::Regex(_)) { + for key in key_list { + if let Value::Text(expr) = key { + match fancy_regex::Regex::new(expr) { + Ok(regex) => { + *key = Value::Regex(Regex::new(expr.to_string(), regex)); + } + Err(err) => { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidRegex(format!("{expr}: {err}")))); + } + } + } + } + } + Ok(()) + } +} + +impl Capability { + pub fn parse(capability: &str) -> Capability { + if let Some(capability) = lookup_capabilities(capability) { + capability + } else if let Some(comparator) = capability.strip_prefix("comparator-") { + Capability::Comparator(Comparator::Other(comparator.to_string())) + } else { + Capability::Other(capability.to_string()) + } + } + + pub fn all() -> &'static [Capability] { + &[ + Capability::Envelope, + Capability::EnvelopeDsn, + Capability::EnvelopeDeliverBy, + Capability::FileInto, + Capability::EncodedCharacter, + Capability::Comparator(Comparator::Elbonia), + Capability::Comparator(Comparator::AsciiCaseMap), + Capability::Comparator(Comparator::AsciiNumeric), + Capability::Comparator(Comparator::Octet), + Capability::Body, + Capability::Convert, + Capability::Copy, + Capability::Relational, + Capability::Date, + Capability::Index, + Capability::Duplicate, + Capability::Variables, + Capability::EditHeader, + Capability::ForEveryPart, + Capability::Mime, + Capability::Replace, + Capability::Enclose, + Capability::ExtractText, + Capability::Enotify, + Capability::RedirectDsn, + Capability::RedirectDeliverBy, + Capability::Environment, + Capability::Reject, + Capability::Ereject, + Capability::ExtLists, + Capability::SubAddress, + Capability::Vacation, + Capability::VacationSeconds, + Capability::Fcc, + Capability::Mailbox, + Capability::MailboxId, + Capability::MboxMetadata, + Capability::ServerMetadata, + Capability::SpecialUse, + Capability::Imap4Flags, + Capability::Ihave, + Capability::ImapSieve, + Capability::Include, + Capability::Regex, + Capability::SpamTest, + Capability::SpamTestPlus, + Capability::VirusTest, + ] + } +} + +fn lookup_relational(input: &str) -> Option { + hashify::tiny_map!( + input.as_bytes(), + "gt" => RelationalMatch::Gt, + "ge" => RelationalMatch::Ge, + "lt" => RelationalMatch::Lt, + "le" => RelationalMatch::Le, + "eq" => RelationalMatch::Eq, + "ne" => RelationalMatch::Ne, + ) +} + +fn lookup_comparator(input: &str) -> Option { + hashify::tiny_map!( + input.as_bytes(), + "i;octet" => Comparator::Octet, + "i;ascii-casemap" => Comparator::AsciiCaseMap, + "i;ascii-numeric" => Comparator::AsciiNumeric, + ) +} + +impl Invalid { + pub fn name(&self) -> &str { + &self.name + } + + pub fn line_num(&self) -> usize { + self.line_num + } + + pub fn line_pos(&self) -> usize { + self.line_pos + } +} + +impl From<&str> for Capability { + fn from(value: &str) -> Self { + Capability::parse(value) + } +} + +impl From for Capability { + fn from(value: String) -> Self { + Capability::parse(&value) + } +} + +impl Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Capability::Envelope => f.write_str("envelope"), + Capability::EnvelopeDsn => f.write_str("envelope-dsn"), + Capability::EnvelopeDeliverBy => f.write_str("envelope-deliverby"), + Capability::FileInto => f.write_str("fileinto"), + Capability::EncodedCharacter => f.write_str("encoded-character"), + Capability::Comparator(Comparator::Elbonia) => f.write_str("comparator-elbonia"), + Capability::Comparator(Comparator::Octet) => f.write_str("comparator-i;octet"), + Capability::Comparator(Comparator::AsciiCaseMap) => { + f.write_str("comparator-i;ascii-casemap") + } + Capability::Comparator(Comparator::AsciiNumeric) => { + f.write_str("comparator-i;ascii-numeric") + } + Capability::Comparator(Comparator::Other(comparator)) => f.write_str(comparator), + Capability::Body => f.write_str("body"), + Capability::Convert => f.write_str("convert"), + Capability::Copy => f.write_str("copy"), + Capability::Relational => f.write_str("relational"), + Capability::Date => f.write_str("date"), + Capability::Index => f.write_str("index"), + Capability::Duplicate => f.write_str("duplicate"), + Capability::Variables => f.write_str("variables"), + Capability::EditHeader => f.write_str("editheader"), + Capability::ForEveryPart => f.write_str("foreverypart"), + Capability::Mime => f.write_str("mime"), + Capability::Replace => f.write_str("replace"), + Capability::Enclose => f.write_str("enclose"), + Capability::ExtractText => f.write_str("extracttext"), + Capability::Enotify => f.write_str("enotify"), + Capability::RedirectDsn => f.write_str("redirect-dsn"), + Capability::RedirectDeliverBy => f.write_str("redirect-deliverby"), + Capability::Environment => f.write_str("environment"), + Capability::Reject => f.write_str("reject"), + Capability::Ereject => f.write_str("ereject"), + Capability::ExtLists => f.write_str("extlists"), + Capability::SubAddress => f.write_str("subaddress"), + Capability::Vacation => f.write_str("vacation"), + Capability::VacationSeconds => f.write_str("vacation-seconds"), + Capability::Fcc => f.write_str("fcc"), + Capability::Mailbox => f.write_str("mailbox"), + Capability::MailboxId => f.write_str("mailboxid"), + Capability::MboxMetadata => f.write_str("mboxmetadata"), + Capability::ServerMetadata => f.write_str("servermetadata"), + Capability::SpecialUse => f.write_str("special-use"), + Capability::Imap4Flags => f.write_str("imap4flags"), + Capability::Ihave => f.write_str("ihave"), + Capability::ImapSieve => f.write_str("imapsieve"), + Capability::Include => f.write_str("include"), + Capability::Regex => f.write_str("regex"), + Capability::SpamTest => f.write_str("spamtest"), + Capability::SpamTestPlus => f.write_str("spamtestplus"), + Capability::VirusTest => f.write_str("virustest"), + Capability::While => f.write_str("vnd.inbuxa.while"), + Capability::Expressions => f.write_str("vnd.inbuxa.expressions"), + Capability::Other(capability) => f.write_str(capability), + } + } +} + +fn lookup_capabilities(input: &str) -> Option { + hashify::tiny_map!( + input.as_bytes(), + "envelope" => Capability::Envelope, + "envelope-dsn" => Capability::EnvelopeDsn, + "envelope-deliverby" => Capability::EnvelopeDeliverBy, + "fileinto" => Capability::FileInto, + "encoded-character" => Capability::EncodedCharacter, + "comparator-elbonia" => Capability::Comparator(Comparator::Elbonia), + "comparator-i;octet" => Capability::Comparator(Comparator::Octet), + "comparator-i;ascii-casemap" => Capability::Comparator(Comparator::AsciiCaseMap), + "comparator-i;ascii-numeric" => Capability::Comparator(Comparator::AsciiNumeric), + "body" => Capability::Body, + "convert" => Capability::Convert, + "copy" => Capability::Copy, + "relational" => Capability::Relational, + "date" => Capability::Date, + "index" => Capability::Index, + "duplicate" => Capability::Duplicate, + "variables" => Capability::Variables, + "editheader" => Capability::EditHeader, + "foreverypart" => Capability::ForEveryPart, + "mime" => Capability::Mime, + "replace" => Capability::Replace, + "enclose" => Capability::Enclose, + "extracttext" => Capability::ExtractText, + "enotify" => Capability::Enotify, + "redirect-dsn" => Capability::RedirectDsn, + "redirect-deliverby" => Capability::RedirectDeliverBy, + "environment" => Capability::Environment, + "reject" => Capability::Reject, + "ereject" => Capability::Ereject, + "extlists" => Capability::ExtLists, + "subaddress" => Capability::SubAddress, + "vacation" => Capability::Vacation, + "vacation-seconds" => Capability::VacationSeconds, + "fcc" => Capability::Fcc, + "mailbox" => Capability::Mailbox, + "mailboxid" => Capability::MailboxId, + "mboxmetadata" => Capability::MboxMetadata, + "servermetadata" => Capability::ServerMetadata, + "special-use" => Capability::SpecialUse, + "imap4flags" => Capability::Imap4Flags, + "ihave" => Capability::Ihave, + "imapsieve" => Capability::ImapSieve, + "include" => Capability::Include, + "regex" => Capability::Regex, + "spamtest" => Capability::SpamTest, + "spamtestplus" => Capability::SpamTestPlus, + "virustest" => Capability::VirusTest, + + // Extensions + "vnd.inbuxa.while" => Capability::While, + "vnd.inbuxa.expressions" => Capability::Expressions, + ) +} diff --git a/vendor/sieve-rs/src/compiler/grammar/test.rs b/vendor/sieve-rs/src/compiler/grammar/test.rs new file mode 100644 index 0000000..fd07145 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/test.rs @@ -0,0 +1,686 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, ErrorType, + lexer::{Token, tokenizer::TokenInfo, word::Word}, +}; + +use super::{ + Capability, Invalid, + actions::{action_convert::Convert, action_vacation::TestVacation}, + expr::{Expression, UnaryOperator, parser::ExpressionParser, tokenizer::Tokenizer}, + instruction::{CompilerState, Instruction}, + tests::{ + test_address::TestAddress, + test_body::TestBody, + test_date::{TestCurrentDate, TestDate}, + test_duplicate::TestDuplicate, + test_envelope::TestEnvelope, + test_exists::TestExists, + test_extlists::TestValidExtList, + test_hasflag::TestHasFlag, + test_header::TestHeader, + test_ihave::TestIhave, + test_mailbox::{TestMailboxExists, TestMetadata, TestMetadataExists}, + test_mailboxid::TestMailboxIdExists, + test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod}, + test_size::TestSize, + test_spamtest::{TestSpamTest, TestVirusTest}, + test_specialuse::TestSpecialUseExists, + test_string::TestString, + }, +}; + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Test { + True, + False, + Address(TestAddress), + Envelope(TestEnvelope), + Exists(TestExists), + Header(TestHeader), + Size(TestSize), + Invalid(Invalid), + + // RFC 5173 + Body(TestBody), + + // RFC 6558 + Convert(Convert), + + // RFC 5260 + Date(TestDate), + CurrentDate(TestCurrentDate), + + // RFC 7352 + Duplicate(TestDuplicate), + + // RFC 5229 & RFC 5183 + String(TestString), + Environment(TestString), + + // RFC 5435 + NotifyMethodCapability(TestNotifyMethodCapability), + ValidNotifyMethod(TestValidNotifyMethod), + + // RFC 6134 + ValidExtList(TestValidExtList), + + // RFC 5463 + Ihave(TestIhave), + + // RFC 5232 + HasFlag(TestHasFlag), + + // RFC 5490 + MailboxExists(TestMailboxExists), + Metadata(TestMetadata), + MetadataExists(TestMetadataExists), + + // RFC 9042 + MailboxIdExists(TestMailboxIdExists), + + // RFC 5235 + SpamTest(TestSpamTest), + VirusTest(TestVirusTest), + + // RFC 8579 + SpecialUseExists(TestSpecialUseExists), + + // RFC 5230 + Vacation(TestVacation), + + // Only test + #[cfg(test)] + TestCmd { + arguments: Vec, + is_not: bool, + }, +} + +#[derive(Debug)] +struct Block { + is_all: bool, + is_not: bool, + p_count: u32, + jmps: Vec, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test(&mut self) -> Result<(), CompileError> { + let mut block_stack: Vec = Vec::new(); + let mut block = Block { + is_all: false, + is_not: false, + p_count: 0, + jmps: Vec::new(), + }; + let mut is_not = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + self.reset_param_check(); + let test: Instruction = + match token_info.token { + Token::Comma + if !block_stack.is_empty() + && matches!( + self.instructions.last(), + Some(Instruction::Test(_) | Instruction::Eval(_)) + ) + && matches!( + self.tokens.peek(), + Some(Ok(TokenInfo { + token: Token::Identifier(_) | Token::Unknown(_), + .. + })) + ) => + { + is_not = block.is_not; + block.jmps.push(self.instructions.len()); + self.instructions.push(if block.is_all { + Instruction::Jz(usize::MAX) + } else { + Instruction::Jnz(usize::MAX) + }); + continue; + } + Token::ParenthesisOpen => { + block.p_count += 1; + continue; + } + Token::ParenthesisClose => { + if block.p_count > 0 { + block.p_count -= 1; + continue; + } else if let Some(prev_block) = block_stack.pop() { + let cur_pos = self.instructions.len(); + for jmp_pos in block.jmps { + if let Instruction::Jnz(jmp_pos) | Instruction::Jz(jmp_pos) = + &mut self.instructions[jmp_pos] + { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened") + } + } + + block = prev_block; + is_not = block.is_not; + if block_stack.is_empty() { + break; + } else { + continue; + } + } else { + return Err(token_info.expected("test name")); + } + } + Token::Identifier(Word::Not) => { + if !matches!( + self.tokens.peek(), + Some(Ok(TokenInfo { + token: Token::Identifier(_) | Token::Unknown(_), + .. + })) + ) { + return Err(token_info.expected("test name")); + } + is_not = !is_not; + continue; + } + Token::Identifier(word @ (Word::AnyOf | Word::AllOf)) => { + if block_stack.len() < self.tokens.compiler.max_nested_tests { + self.tokens.expect_token(Token::ParenthesisOpen)?; + block_stack.push(block); + let (is_all, block_is_not) = if word == Word::AllOf { + if !is_not { + (true, false) + } else { + (false, true) + } + } else if !is_not { + (false, false) + } else { + (true, true) + }; + block = Block { + is_all, + is_not: block_is_not, + p_count: 0, + jmps: Vec::new(), + }; + is_not = block_is_not; + continue; + } else { + return Err(CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type: ErrorType::TooManyNestedTests, + }); + } + } + Token::Identifier(Word::True) => if !is_not { + Test::True + } else { + is_not = false; + Test::False + } + .into(), + Token::Identifier(Word::False) => if !is_not { + Test::False + } else { + is_not = false; + Test::True + } + .into(), + Token::Identifier(Word::Address) => self.parse_test_address()?.into(), + Token::Identifier(Word::Envelope) => { + self.validate_argument( + 0, + Capability::Envelope.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_envelope()?.into() + } + Token::Identifier(Word::Header) => self.parse_test_header()?.into(), + Token::Identifier(Word::Size) => self.parse_test_size()?.into(), + Token::Identifier(Word::Exists) => self.parse_test_exists()?.into(), + + // RFC 5173 + Token::Identifier(Word::Body) => { + self.validate_argument( + 0, + Capability::Body.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_body()?.into() + } + + // RFC 6558 + Token::Identifier(Word::Convert) => { + self.validate_argument( + 0, + Capability::Convert.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_convert()?.into() + } + + // RFC 5260 + Token::Identifier(Word::Date) => { + self.validate_argument( + 0, + Capability::Date.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_date()?.into() + } + Token::Identifier(Word::CurrentDate) => { + self.validate_argument( + 0, + Capability::Date.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_currentdate()?.into() + } + + // RFC 7352 + Token::Identifier(Word::Duplicate) => { + self.validate_argument( + 0, + Capability::Duplicate.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_duplicate()?.into() + } + + // RFC 5229 + Token::Identifier(Word::String) => { + self.validate_argument( + 0, + Capability::Variables.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_string()?.into() + } + + // RFC 5435 + Token::Identifier(Word::NotifyMethodCapability) => { + self.validate_argument( + 0, + Capability::Enotify.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_notify_method_capability()?.into() + } + Token::Identifier(Word::ValidNotifyMethod) => { + self.validate_argument( + 0, + Capability::Enotify.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_valid_notify_method()?.into() + } + + // RFC 5183 + Token::Identifier(Word::Environment) => { + self.validate_argument( + 0, + Capability::Environment.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_environment()?.into() + } + + // RFC 6134 + Token::Identifier(Word::ValidExtList) => { + self.validate_argument( + 0, + Capability::ExtLists.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_valid_ext_list()?.into() + } + + // RFC 5463 + Token::Identifier(Word::Ihave) => { + self.validate_argument( + 0, + Capability::Ihave.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_ihave()?.into() + } + + // RFC 5232 + Token::Identifier(Word::HasFlag) => { + self.validate_argument( + 0, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_hasflag()?.into() + } + + // RFC 5490 + Token::Identifier(Word::MailboxExists) => { + self.validate_argument( + 0, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_mailboxexists()?.into() + } + Token::Identifier(Word::Metadata) => { + self.validate_argument( + 0, + Capability::MboxMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_metadata()?.into() + } + Token::Identifier(Word::MetadataExists) => { + self.validate_argument( + 0, + Capability::MboxMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_metadataexists()?.into() + } + Token::Identifier(Word::ServerMetadata) => { + self.validate_argument( + 0, + Capability::ServerMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_servermetadata()?.into() + } + Token::Identifier(Word::ServerMetadataExists) => { + self.validate_argument( + 0, + Capability::ServerMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_servermetadataexists()?.into() + } + + // RFC 9042 + Token::Identifier(Word::MailboxIdExists) => { + self.validate_argument( + 0, + Capability::MailboxId.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_mailboxidexists()?.into() + } + + // RFC 5235 + Token::Identifier(Word::SpamTest) => { + self.validate_argument( + 0, + Capability::SpamTest.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_spamtest()?.into() + } + Token::Identifier(Word::VirusTest) => { + self.validate_argument( + 0, + Capability::VirusTest.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_virustest()?.into() + } + + // RFC 8579 + Token::Identifier(Word::SpecialUseExists) => { + self.validate_argument( + 0, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_specialuseexists()?.into() + } + + // Expressions extension + Token::Identifier(Word::Eval) => { + self.validate_argument( + 0, + Capability::Expressions.into(), + token_info.line_num, + token_info.line_pos, + )?; + + Instruction::Eval(self.parse_expr()?) + } + Token::Identifier(word) => { + self.ignore_test()?; + Test::Invalid(Invalid { + name: word.to_string(), + line_num: token_info.line_num, + line_pos: token_info.line_pos, + }) + .into() + } + #[cfg(test)] + Token::Unknown(name) if name.contains("test") => { + use crate::compiler::Value; + + let mut arguments = Vec::new(); + arguments.push(Value::Text(name.into())); + while !matches!( + self.tokens.peek().map(|r| r.map(|t| &t.token)), + Some(Ok(Token::Comma + | Token::ParenthesisClose + | Token::CurlyOpen)) + ) { + arguments.push(match self.tokens.unwrap_next()?.token { + Token::StringConstant(s) => Value::from(s), + Token::StringVariable(s) => self + .tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: 0, + line_pos: 0, + error_type, + })?, + Token::Number(n) => { + Value::Number(crate::compiler::Number::Integer(n as i64)) + } + Token::Identifier(s) => Value::Text(s.to_string().into()), + Token::Tag(s) => Value::Text(format!(":{s}").into()), + Token::Unknown(s) => Value::Text(s.into()), + other => panic!("Invalid test param {other:?}"), + }); + } + Test::TestCmd { + arguments, + is_not: false, + } + .into() + } + Token::Unknown(name) => { + self.ignore_test()?; + Test::Invalid(Invalid { + name, + line_num: token_info.line_num, + line_pos: token_info.line_pos, + }) + .into() + } + _ => return Err(token_info.expected("test name")), + }; + + while block.p_count > 0 { + self.tokens.expect_token(Token::ParenthesisClose)?; + block.p_count -= 1; + } + + self.instructions + .push(if !is_not { test } else { test.set_not() }); + + if block_stack.is_empty() { + break; + } + } + + self.instructions.push(Instruction::Jz(usize::MAX)); + Ok(()) + } + + pub(crate) fn parse_expr(&mut self) -> Result, CompileError> { + let mut next_token = self.tokens.unwrap_next()?; + let expr = match next_token.token { + Token::StringConstant(s) => s.into_string().into_bytes(), + Token::StringVariable(s) => s, + _ => return Err(next_token.expected("string")), + }; + + match ExpressionParser::from_tokenizer(Tokenizer::from_iter( + expr.iter().enumerate().peekable(), + |var_name, maybe_namespace| self.parse_expr_fnc_or_var(var_name, maybe_namespace), + )) + .parse() + { + Ok(parser) => Ok(parser.output), + Err(err) => { + let err = ErrorType::InvalidExpression(format!( + "{}: {}", + std::str::from_utf8(&expr).unwrap_or_default(), + err + )); + next_token.token = Token::StringVariable(expr); + Err(next_token.custom(err)) + } + } + } +} + +impl From for Instruction { + fn from(test: Test) -> Self { + Instruction::Test(test) + } +} + +impl Instruction { + pub fn set_not(mut self) -> Self { + match &mut self { + Instruction::Test(test) => match test { + Test::True => return Instruction::Test(Test::False), + Test::False => return Instruction::Test(Test::True), + Test::Address(op) => { + op.is_not = true; + } + Test::Envelope(op) => { + op.is_not = true; + } + Test::Exists(op) => { + op.is_not = true; + } + Test::Header(op) => { + op.is_not = true; + } + Test::Size(op) => { + op.is_not = true; + } + Test::Body(op) => { + op.is_not = true; + } + Test::Convert(op) => { + op.is_not = true; + } + Test::Date(op) => { + op.is_not = true; + } + Test::CurrentDate(op) => { + op.is_not = true; + } + Test::Duplicate(op) => { + op.is_not = true; + } + Test::String(op) | Test::Environment(op) => { + op.is_not = true; + } + Test::NotifyMethodCapability(op) => { + op.is_not = true; + } + Test::ValidNotifyMethod(op) => { + op.is_not = true; + } + Test::ValidExtList(op) => { + op.is_not = true; + } + Test::Ihave(op) => { + op.is_not = true; + } + Test::HasFlag(op) => { + op.is_not = true; + } + Test::MailboxExists(op) => { + op.is_not = true; + } + Test::Metadata(op) => { + op.is_not = true; + } + Test::MetadataExists(op) => { + op.is_not = true; + } + Test::MailboxIdExists(op) => { + op.is_not = true; + } + Test::SpamTest(op) => { + op.is_not = true; + } + Test::VirusTest(op) => { + op.is_not = true; + } + Test::SpecialUseExists(op) => { + op.is_not = true; + } + #[cfg(test)] + Test::TestCmd { is_not, .. } => { + *is_not = true; + } + Test::Vacation(_) | Test::Invalid(_) => {} + }, + Instruction::Eval(expr) => expr.push(Expression::UnaryOperator(UnaryOperator::Not)), + _ => (), + } + self + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/mod.rs b/vendor/sieve-rs/src/compiler/grammar/tests/mod.rs new file mode 100644 index 0000000..1a7520e --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/mod.rs @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod test_address; +pub mod test_body; +pub mod test_date; +pub mod test_duplicate; +pub mod test_envelope; +pub mod test_environment; +pub mod test_exists; +pub mod test_extlists; +pub mod test_hasflag; +pub mod test_header; +pub mod test_ihave; +pub mod test_mailbox; +pub mod test_mailboxid; +pub mod test_notify; +pub mod test_size; +pub mod test_spamtest; +pub mod test_specialuse; +pub mod test_string; diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_address.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_address.rs new file mode 100644 index 0000000..43eeb7d --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_address.rs @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{Capability, Comparator, instruction::CompilerState, test::Test}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{AddressPart, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestAddress { + pub header_list: Vec, + pub key_list: Vec, + pub address_part: AddressPart, + pub match_type: MatchType, + pub comparator: Comparator, + pub index: Option, + + pub mime_anychild: bool, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_address(&mut self) -> Result { + let mut address_part = AddressPart::All; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut header_list = None; + let mut key_list; + let mut index = None; + let mut index_last = false; + + let mut mime = false; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::LocalPart + | Word::Domain + | Word::All + | Word::User + | Word::Detail + | Word::Name), + ) => { + self.validate_argument( + 1, + if matches!(word, Word::User | Word::Detail) { + Capability::SubAddress.into() + } else { + None + }, + token_info.line_num, + token_info.line_pos, + )?; + address_part = word.into(); + } + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 2, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument( + 4, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument( + 5, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 7, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + _ => { + if header_list.is_none() { + header_list = self.parse_strings_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Address(TestAddress { + header_list: header_list.unwrap(), + key_list, + address_part, + match_type, + comparator, + index: if index_last { index.map(|i| -i) } else { index }, + mime_anychild, + is_not: false, + })) + } +} + +impl From for AddressPart { + fn from(word: Word) -> Self { + match word { + Word::LocalPart => AddressPart::LocalPart, + Word::Domain => AddressPart::Domain, + Word::All => AddressPart::All, + Word::User => AddressPart::User, + Word::Detail => AddressPart::Detail, + Word::Name => AddressPart::Name, + _ => unreachable!(), + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_body.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_body.rs new file mode 100644 index 0000000..7f41653 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_body.rs @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestBody { + pub key_list: Vec, + pub body_transform: BodyTransform, + pub match_type: MatchType, + pub comparator: Comparator, + pub include_subject: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum BodyTransform { + Raw, + Content(Vec), + Text, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_body(&mut self) -> Result { + let mut body_transform = BodyTransform::Text; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut key_list; + let mut include_subject = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Raw) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + body_transform = BodyTransform::Raw; + } + Token::Tag(Word::Text) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + body_transform = BodyTransform::Text; + } + Token::Tag(Word::Content) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + body_transform = BodyTransform::Content(self.parse_strings(false)?); + } + Token::Tag(Word::Subject) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + include_subject = true; + } + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 2, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Body(TestBody { + key_list, + body_transform, + match_type, + comparator, + include_subject, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_date.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_date.rs new file mode 100644 index 0000000..23ffe17 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_date.rs @@ -0,0 +1,368 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::HeaderName; + +use crate::compiler::{ + CompileError, ErrorType, Number, Value, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{StringConstant, Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestDate { + pub header_name: Value, + pub key_list: Vec, + pub match_type: MatchType, + pub comparator: Comparator, + pub index: Option, + pub zone: Zone, + pub date_part: DatePart, + pub mime_anychild: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestCurrentDate { + pub zone: Option, + pub match_type: MatchType, + pub comparator: Comparator, + pub date_part: DatePart, + pub key_list: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum Zone { + Time(i64), + Original, + Local, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum DatePart { + Year, + Month, + Day, + Date, + Julian, + Hour, + Minute, + Second, + Time, + Iso8601, + Std11, + Zone, + Weekday, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_date(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut header_name = None; + let mut key_list; + let mut index = None; + let mut index_last = false; + let mut zone = Zone::Local; + let mut date_part = None; + + let mut mime = false; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument( + 3, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument( + 4, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 5, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + Token::Tag(Word::OriginalZone) => { + self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?; + zone = Zone::Original; + } + Token::Tag(Word::Zone) => { + self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?; + zone = Zone::Time(self.parse_timezone()?); + } + _ => { + if header_name.is_none() { + let header = self.parse_string_token(token_info)?; + if let Value::Text(header_name) = &header + && HeaderName::parse(header_name.as_ref()).is_none() + { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + header_name = header.into(); + } else if date_part.is_none() { + if let Token::StringConstant(string) = &token_info.token + && let Some(date_part_) = + lookup_date_part(&string.to_string().to_ascii_lowercase()) + { + date_part = date_part_.into(); + continue; + } + return Err(token_info.expected("valid date part")); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Date(TestDate { + header_name: header_name.unwrap(), + key_list, + date_part: date_part.unwrap(), + match_type, + comparator, + index: if index_last { index.map(|i| -i) } else { index }, + zone, + mime_anychild, + is_not: false, + })) + } + + pub(crate) fn parse_test_currentdate(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut key_list; + let mut zone = None; + let mut date_part = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Zone) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + zone = self.parse_timezone()?.into(); + } + _ => { + if date_part.is_none() { + if let Token::StringConstant(string) = &token_info.token + && let Some(date_part_) = + lookup_date_part(&string.to_string().to_ascii_lowercase()) + { + date_part = date_part_.into(); + continue; + } + return Err(token_info.expected("valid date part")); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::CurrentDate(TestCurrentDate { + key_list, + date_part: date_part.unwrap(), + match_type, + comparator, + zone, + is_not: false, + })) + } + + pub(crate) fn parse_timezone(&mut self) -> Result { + let token_info = self.tokens.unwrap_next()?; + if let Token::StringConstant(value) = &token_info.token { + let timezone = match value { + StringConstant::String(value) => value.parse::().unwrap_or(i64::MAX), + StringConstant::Number(Number::Integer(n)) => *n, + StringConstant::Number(Number::Float(n)) => *n as i64, + }; + + return match timezone { + 0..=1400 => Ok((timezone / 100 * 3600) + (timezone % 100 * 60)), + -1200..=-1 => Ok((timezone / 100 * 3600) - (-timezone % 100 * 60)), + _ => Err(token_info.expected("invalid timezone")), + }; + } + Err(token_info.expected("string containing time zone")) + } +} + +/* + "year" => the year, "0000" .. "9999". + "month" => the month, "01" .. "12". + "day" => the day, "01" .. "31". + "date" => the date in "yyyy-mm-dd" format. + "julian" => the Modified Julian Day, that is, the date + expressed as an integer number of days since + 00:00 UTC on November 17, 1858 (using the Gregorian + calendar). This corresponds to the regular + Julian Day minus 2400000.5. Sample routines to + convert to and from modified Julian dates are + given in Appendix A. + "hour" => the hour, "00" .. "23". + "minute" => the minute, "00" .. "59". + "second" => the second, "00" .. "60". + "time" => the time in "hh:mm:ss" format. + "iso8601" => the date and time in restricted ISO 8601 format. + "std11" => the date and time in a format appropriate + for use in a Date: header field [RFC2822]. + "zone" => the time zone in use. If the user specified a + time zone with ":zone", "zone" will + contain that value. If :originalzone is specified + this value will be the original zone specified + in the date-time value. If neither argument is + specified the value will be the server's default + time zone in offset format "+hhmm" or "-hhmm". An + offset of 0 (Zulu) always has a positive sign. + "weekday" => the day of the week expressed as an integer between + "0" and "6". "0" is Sunday, "1" is Monday, etc. +*/ + +fn lookup_date_part(input: &str) -> Option { + hashify::tiny_map!( + input.as_bytes(), + "year" => DatePart::Year, + "month" => DatePart::Month, + "day" => DatePart::Day, + "date" => DatePart::Date, + "julian" => DatePart::Julian, + "hour" => DatePart::Hour, + "minute" => DatePart::Minute, + "second" => DatePart::Second, + "time" => DatePart::Time, + "iso8601" => DatePart::Iso8601, + "std11" => DatePart::Std11, + "zone" => DatePart::Zone, + "weekday" => DatePart::Weekday, + ) +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_duplicate.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_duplicate.rs new file mode 100644 index 0000000..485eab6 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_duplicate.rs @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::HeaderName; + +use crate::compiler::{ + CompileError, ErrorType, Value, + grammar::instruction::{CompilerState, MapLocalVars}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestDuplicate { + pub handle: Option, + pub dup_match: DupMatch, + pub seconds: Option, + pub last: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) enum DupMatch { + Header(Value), + UniqueId(Value), + Default, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_duplicate(&mut self) -> Result { + let mut handle = None; + let mut dup_match = DupMatch::Default; + let mut seconds = None; + let mut last = false; + + while let Some(token_info) = self.tokens.peek() { + let token_info = token_info?; + let line_num = token_info.line_num; + let line_pos = token_info.line_pos; + + match token_info.token { + Token::Tag(Word::Handle) => { + self.validate_argument(1, None, line_num, line_pos)?; + self.tokens.next(); + handle = self.parse_string()?.into(); + } + Token::Tag(Word::Header) => { + self.validate_argument(2, None, line_num, line_pos)?; + self.tokens.next(); + let header = self.parse_string()?; + if let Value::Text(header_name) = &header + && HeaderName::parse(header_name.as_ref()).is_none() + { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + dup_match = DupMatch::Header(header); + } + Token::Tag(Word::UniqueId) => { + self.validate_argument(2, None, line_num, line_pos)?; + self.tokens.next(); + dup_match = DupMatch::UniqueId(self.parse_string()?); + } + Token::Tag(Word::Seconds) => { + self.validate_argument(3, None, line_num, line_pos)?; + self.tokens.next(); + seconds = (self.tokens.expect_number(u64::MAX as usize)? as u64).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument(4, None, line_num, line_pos)?; + self.tokens.next(); + last = true; + } + _ => break, + } + } + + Ok(Test::Duplicate(TestDuplicate { + handle, + dup_match, + seconds, + last, + is_not: false, + })) + } +} + +impl MapLocalVars for DupMatch { + fn map_local_vars(&mut self, last_id: usize) { + match self { + DupMatch::Header(header) => header.map_local_vars(last_id), + DupMatch::UniqueId(unique_id) => unique_id.map_local_vars(last_id), + DupMatch::Default => {} + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_envelope.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_envelope.rs new file mode 100644 index 0000000..04851c6 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_envelope.rs @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + Envelope, + compiler::{ + CompileError, ErrorType, Value, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, word::Word}, + }, +}; + +use crate::compiler::grammar::{AddressPart, MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestEnvelope { + pub envelope_list: Vec, + pub key_list: Vec, + pub address_part: AddressPart, + pub match_type: MatchType, + pub comparator: Comparator, + pub zone: Option, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_envelope(&mut self) -> Result { + let mut address_part = AddressPart::All; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut envelope_list = None; + let mut key_list; + let mut zone = None; + + loop { + let mut token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::LocalPart | Word::Domain | Word::All | Word::User | Word::Detail), + ) => { + self.validate_argument( + 1, + if matches!(word, Word::User | Word::Detail) { + Capability::SubAddress.into() + } else { + None + }, + token_info.line_num, + token_info.line_pos, + )?; + address_part = word.into(); + } + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 2, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Zone) => { + self.validate_argument( + 4, + Capability::EnvelopeDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + zone = self.parse_timezone()?.into(); + } + _ => { + if envelope_list.is_none() { + let mut envelopes = Vec::new(); + let line_num = token_info.line_num; + let line_pos = token_info.line_pos; + + match token_info.token { + Token::StringConstant(s) => { + match Envelope::try_from(s.into_string().to_ascii_lowercase()) { + Ok(envelope) => { + envelopes.push(envelope); + } + Err(invalid) => { + token_info.token = Token::Comma; + return Err( + token_info.custom(ErrorType::InvalidEnvelope(invalid)) + ); + } + } + } + Token::BracketOpen => loop { + let mut token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(s) => { + match Envelope::try_from( + s.into_string().to_ascii_lowercase(), + ) { + Ok(envelope) => { + if !envelopes.contains(&envelope) { + envelopes.push(envelope); + } + } + Err(invalid) => { + token_info.token = Token::Comma; + return Err(token_info + .custom(ErrorType::InvalidEnvelope(invalid))); + } + } + } + Token::Comma => (), + Token::BracketClose if !envelopes.is_empty() => break, + _ => return Err(token_info.expected("constant string")), + } + }, + _ => return Err(token_info.expected("constant string")), + } + + for envelope in &envelopes { + match envelope { + Envelope::ByTimeAbsolute + | Envelope::ByTimeRelative + | Envelope::ByMode + | Envelope::ByTrace => { + self.validate_argument( + 0, + Capability::EnvelopeDeliverBy.into(), + line_num, + line_pos, + )?; + } + + Envelope::Notify + | Envelope::Orcpt + | Envelope::Ret + | Envelope::Envid => { + self.validate_argument( + 0, + Capability::EnvelopeDsn.into(), + line_num, + line_pos, + )?; + } + _ => (), + } + } + + envelope_list = envelopes.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Envelope(TestEnvelope { + envelope_list: envelope_list.unwrap(), + key_list, + address_part, + match_type, + comparator, + zone, + is_not: false, + })) + } +} + +impl TryFrom for Envelope { + type Error = String; + + fn try_from(value: String) -> Result { + if let Some(envelope) = lookup_envelope(&value) { + Ok(envelope) + } else { + Err(value) + } + } +} + +impl<'x> TryFrom<&'x str> for Envelope { + type Error = &'x str; + + fn try_from(value: &'x str) -> Result { + if let Some(envelope) = lookup_envelope(value) { + Ok(envelope) + } else { + Err(value) + } + } +} + +fn lookup_envelope(input: &str) -> Option { + hashify::tiny_map!( + input.as_bytes(), + "from" => Envelope::From, + "to" => Envelope::To, + "bytimeabsolute" => Envelope::ByTimeAbsolute, + "bytimerelative" => Envelope::ByTimeRelative, + "bymode" => Envelope::ByMode, + "bytrace" => Envelope::ByTrace, + "notify" => Envelope::Notify, + "orcpt" => Envelope::Orcpt, + "ret" => Envelope::Ret, + "envid" => Envelope::Envid, + ) +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_environment.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_environment.rs new file mode 100644 index 0000000..9f8973f --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_environment.rs @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, VariableType, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +use super::test_string::TestString; + +impl CompilerState<'_> { + pub(crate) fn parse_test_environment(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut name = None; + let mut key_list; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if name.is_none() { + if let Token::StringConstant(s) = token_info.token { + name = Value::Variable(VariableType::Environment( + s.into_string().to_lowercase(), + )) + .into(); + } else { + return Err(token_info.expected("environment variable")); + } + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Environment(TestString { + source: vec![name.unwrap()], + key_list, + match_type, + comparator, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_exists.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_exists.rs new file mode 100644 index 0000000..80f882d --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_exists.rs @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::HeaderName; + +use crate::compiler::{ + CompileError, ErrorType, Value, + grammar::{Capability, instruction::CompilerState}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestExists { + pub header_names: Vec, + pub mime_anychild: bool, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_exists(&mut self) -> Result { + let mut header_names = None; + + let mut mime = false; + let mut mime_anychild = false; + + while header_names.is_none() { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Mime) => { + self.validate_argument( + 1, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 2, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + _ => { + let headers = self.parse_strings_token(token_info)?; + for header in &headers { + if let Value::Text(header_name) = &header + && HeaderName::parse(header_name.as_ref()).is_none() + { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + header_names = headers.into(); + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + + Ok(Test::Exists(TestExists { + header_names: header_names.unwrap(), + mime_anychild, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_extlists.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_extlists.rs new file mode 100644 index 0000000..b37f46f --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_extlists.rs @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::CompileError; +use crate::compiler::Value; +use crate::compiler::grammar::instruction::CompilerState; + +use crate::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestValidExtList { + pub list_names: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_valid_ext_list(&mut self) -> Result { + Ok(Test::ValidExtList(TestValidExtList { + list_names: self.parse_strings(false)?, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_hasflag.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_hasflag.rs new file mode 100644 index 0000000..5aa8236 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_hasflag.rs @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, ErrorType, Value, VariableType, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, tokenizer::TokenInfo, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +/* + Usage: hasflag [MATCH-TYPE] [COMPARATOR] + [] + +*/ + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestHasFlag { + pub comparator: Comparator, + pub match_type: MatchType, + pub variable_list: Vec, + pub flags: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_hasflag(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut is_local = false; + + let mut maybe_variables; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Local) => { + is_local = true; + } + _ => { + maybe_variables = self.parse_strings_token(token_info)?; + break; + } + } + } + + match self.tokens.peek() { + Some(Ok(TokenInfo { + token: Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen, + line_num, + line_pos, + })) => { + if !maybe_variables.is_empty() { + let line_num = *line_num; + let line_pos = *line_pos; + + let mut variable_list = Vec::with_capacity(maybe_variables.len()); + for variable in maybe_variables { + match variable { + Value::Text(var_name) => { + variable_list.push( + self.register_variable(var_name.to_string(), is_local) + .map_err(|error_type| CompileError { + line_num, + line_pos, + error_type, + })?, + ); + } + _ => { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::ExpectedConstantString)); + } + } + } + let mut flags = self.parse_strings(false)?; + self.validate_match(&match_type, &mut flags)?; + + Ok(Test::HasFlag(TestHasFlag { + comparator, + match_type, + variable_list, + flags, + is_not: false, + })) + } else { + Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::ExpectedConstantString)) + } + } + _ => { + self.validate_match(&match_type, &mut maybe_variables)?; + + Ok(Test::HasFlag(TestHasFlag { + comparator, + match_type, + variable_list: Vec::new(), + flags: maybe_variables, + is_not: false, + })) + } + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_header.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_header.rs new file mode 100644 index 0000000..975ae9b --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_header.rs @@ -0,0 +1,181 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::HeaderName; + +use crate::compiler::{ + CompileError, ErrorType, Value, + grammar::{ + Capability, Comparator, + actions::action_mime::MimeOpts, + instruction::{CompilerState, MapLocalVars}, + }, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestHeader { + pub header_list: Vec, + pub key_list: Vec, + pub match_type: MatchType, + pub comparator: Comparator, + pub index: Option, + + pub mime_opts: MimeOpts, + pub mime_anychild: bool, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_header(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut header_list = None; + let mut key_list; + let mut index = None; + let mut index_last = false; + + let mut mime = false; + let mut mime_opts = MimeOpts::None; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument( + 3, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument( + 4, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 5, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + Token::Tag( + word @ (Word::Type | Word::Subtype | Word::ContentType | Word::Param), + ) => { + self.validate_argument( + 7, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_opts = self.parse_mimeopts(word)?; + } + _ => { + if header_list.is_none() { + let headers = self.parse_strings_token(token_info)?; + for header in &headers { + if let Value::Text(header_name) = &header + && HeaderName::parse(header_name.as_ref()).is_none() + { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + header_list = headers.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + + if !mime && (mime_anychild || mime_opts != MimeOpts::None) { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Header(TestHeader { + header_list: header_list.unwrap(), + key_list, + match_type, + comparator, + index: if index_last { index.map(|i| -i) } else { index }, + mime_opts, + mime_anychild, + is_not: false, + })) + } +} + +impl MapLocalVars for MimeOpts { + fn map_local_vars(&mut self, last_id: usize) { + if let MimeOpts::Param(value) = self { + value.map_local_vars(last_id) + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_ihave.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_ihave.rs new file mode 100644 index 0000000..49fcc0f --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_ihave.rs @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::CompileError; +use crate::compiler::Value; +use crate::compiler::grammar::Capability; +use crate::compiler::grammar::instruction::{CompilerState, Instruction}; + +use crate::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestIhave { + pub capabilities: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct Error { + pub message: Value, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_ihave(&mut self) -> Result { + Ok(Test::Ihave(TestIhave { + capabilities: self + .parse_static_strings()? + .into_iter() + .map(|n| n.into()) + .collect(), + is_not: false, + })) + } + + pub(crate) fn parse_error(&mut self) -> Result<(), CompileError> { + let cmd = Instruction::Error(Error { + message: self.parse_string()?, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_mailbox.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_mailbox.rs new file mode 100644 index 0000000..9227826 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_mailbox.rs @@ -0,0 +1,239 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + Metadata, + compiler::{ + CompileError, Value, + grammar::{ + Capability, Comparator, + instruction::{CompilerState, MapLocalVars}, + }, + lexer::{Token, word::Word}, + }, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestMailboxExists { + pub mailbox_names: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestMetadataExists { + pub mailbox: Option, + pub annotation_names: Vec, + pub is_not: bool, +} + +/* + +metadata [MATCH-TYPE] [COMPARATOR] + + + +*/ + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestMetadata { + pub match_type: MatchType, + pub comparator: Comparator, + pub medatata: Metadata, + pub key_list: Vec, + pub is_not: bool, +} + +/* + +servermetadata [MATCH-TYPE] [COMPARATOR] + + +*/ + +impl CompilerState<'_> { + pub(crate) fn parse_test_mailboxexists(&mut self) -> Result { + Ok(Test::MailboxExists(TestMailboxExists { + mailbox_names: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_metadataexists(&mut self) -> Result { + Ok(Test::MetadataExists(TestMetadataExists { + mailbox: self.parse_string()?.into(), + annotation_names: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_servermetadataexists(&mut self) -> Result { + Ok(Test::MetadataExists(TestMetadataExists { + mailbox: None, + annotation_names: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_metadata(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut mailbox = None; + let mut annotation_name = None; + let mut key_list: Vec; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if mailbox.is_none() { + mailbox = self.parse_string_token(token_info)?.into(); + } else if annotation_name.is_none() { + annotation_name = self.parse_string_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Metadata(TestMetadata { + match_type, + comparator, + medatata: Metadata::Mailbox { + name: mailbox.unwrap(), + annotation: annotation_name.unwrap(), + }, + key_list, + is_not: false, + })) + } + + pub(crate) fn parse_test_servermetadata(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut annotation_name = None; + let mut key_list: Vec; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if annotation_name.is_none() { + annotation_name = self.parse_string_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Metadata(TestMetadata { + match_type, + comparator, + medatata: Metadata::Server { + annotation: annotation_name.unwrap(), + }, + key_list, + is_not: false, + })) + } +} + +impl MapLocalVars for Metadata { + fn map_local_vars(&mut self, last_id: usize) { + match self { + Metadata::Mailbox { name, annotation } => { + name.map_local_vars(last_id); + annotation.map_local_vars(last_id); + } + Metadata::Server { annotation } => { + annotation.map_local_vars(last_id); + } + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_mailboxid.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_mailboxid.rs new file mode 100644 index 0000000..4c64b41 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_mailboxid.rs @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::CompileError; +use crate::compiler::Value; +use crate::compiler::grammar::instruction::CompilerState; + +use crate::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestMailboxIdExists { + pub mailbox_ids: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_mailboxidexists(&mut self) -> Result { + Ok(Test::MailboxIdExists(TestMailboxIdExists { + mailbox_ids: self.parse_strings(false)?, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_notify.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_notify.rs new file mode 100644 index 0000000..304c658 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_notify.rs @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestNotifyMethodCapability { + pub comparator: Comparator, + pub match_type: MatchType, + pub notification_uri: Value, + pub notification_capability: Value, + pub key_list: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestValidNotifyMethod { + pub notification_uris: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_valid_notify_method(&mut self) -> Result { + Ok(Test::ValidNotifyMethod(TestValidNotifyMethod { + notification_uris: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_notify_method_capability(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut notification_uri = None; + let mut notification_capability = None; + let mut key_list; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if notification_uri.is_none() { + notification_uri = self.parse_string_token(token_info)?.into(); + } else if notification_capability.is_none() { + notification_capability = self.parse_string_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::NotifyMethodCapability(TestNotifyMethodCapability { + key_list, + match_type, + comparator, + notification_uri: notification_uri.unwrap(), + notification_capability: notification_capability.unwrap(), + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_size.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_size.rs new file mode 100644 index 0000000..6fe8b77 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_size.rs @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, + grammar::instruction::CompilerState, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestSize { + pub over: bool, + pub limit: usize, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_size(&mut self) -> Result { + let token_info = self.tokens.unwrap_next()?; + let over = match token_info.token { + Token::Tag(Word::Over) => true, + Token::Tag(Word::Under) => false, + _ => { + return Err(token_info.expected("':over' or ':under'")); + } + }; + let token_info = self.tokens.unwrap_next()?; + if let Token::Number(limit) = token_info.token { + Ok(Test::Size(TestSize { + over, + limit, + is_not: false, + })) + } else { + Err(token_info.expected("number")) + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_spamtest.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_spamtest.rs new file mode 100644 index 0000000..2443a76 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_spamtest.rs @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +/* + Usage: spamtest [":percent"] [COMPARATOR] [MATCH-TYPE] + +*/ + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestSpamTest { + pub value: Value, + pub match_type: MatchType, + pub comparator: Comparator, + pub percent: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestVirusTest { + pub value: Value, + pub match_type: MatchType, + pub comparator: Comparator, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_spamtest(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut percent = false; + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Percent) => { + self.validate_argument( + 3, + Capability::SpamTestPlus.into(), + token_info.line_num, + token_info.line_pos, + )?; + percent = true; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + Ok(Test::SpamTest(TestSpamTest { + value, + percent, + match_type, + comparator, + is_not: false, + })) + } + + pub(crate) fn parse_test_virustest(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + Ok(Test::VirusTest(TestVirusTest { + value, + match_type, + comparator, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_specialuse.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_specialuse.rs new file mode 100644 index 0000000..c9c94fb --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_specialuse.rs @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{CompileError, Value, grammar::instruction::CompilerState, lexer::Token}; + +use crate::compiler::grammar::test::Test; + +/* + Usage: specialuse_exists [] + +*/ + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestSpecialUseExists { + pub mailbox: Option, + pub attributes: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_specialuseexists(&mut self) -> Result { + let mut maybe_attributes = self.parse_strings(false)?; + + match self.tokens.peek().map(|r| r.map(|t| &t.token)) { + Some(Ok(Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen)) => { + if maybe_attributes.len() == 1 { + Ok(Test::SpecialUseExists(TestSpecialUseExists { + mailbox: maybe_attributes.pop(), + attributes: self.parse_strings(false)?, + is_not: false, + })) + } else { + Err(self.tokens.unwrap_next()?.expected("string")) + } + } + _ => Ok(Test::SpecialUseExists(TestSpecialUseExists { + mailbox: None, + attributes: maybe_attributes, + is_not: false, + })), + } + } +} diff --git a/vendor/sieve-rs/src/compiler/grammar/tests/test_string.rs b/vendor/sieve-rs/src/compiler/grammar/tests/test_string.rs new file mode 100644 index 0000000..3231647 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/grammar/tests/test_string.rs @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::compiler::{ + CompileError, Value, + grammar::{Capability, Comparator, instruction::CompilerState}, + lexer::{Token, word::Word}, +}; + +use crate::compiler::grammar::{MatchType, test::Test}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct TestString { + pub match_type: MatchType, + pub comparator: Comparator, + pub source: Vec, + pub key_list: Vec, + pub is_not: bool, +} + +impl CompilerState<'_> { + pub(crate) fn parse_test_string(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut source = None; + let mut key_list: Vec; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if source.is_none() { + source = self.parse_strings_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::String(TestString { + source: source.unwrap(), + key_list, + match_type, + comparator, + is_not: false, + })) + } +} diff --git a/vendor/sieve-rs/src/compiler/lexer/mod.rs b/vendor/sieve-rs/src/compiler/lexer/mod.rs new file mode 100644 index 0000000..f08e125 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/lexer/mod.rs @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod string; +pub mod tokenizer; +pub mod word; + +use std::{borrow::Cow, fmt::Display}; + +use self::word::Word; + +use super::{Number, Value}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Token { + CurlyOpen, + CurlyClose, + BracketOpen, + BracketClose, + ParenthesisOpen, + ParenthesisClose, + Comma, + Semicolon, + StringConstant(StringConstant), + StringVariable(Vec), + Number(usize), + Identifier(Word), + Tag(Word), + Unknown(String), + Colon, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum StringConstant { + String(String), + Number(Number), +} + +impl StringConstant { + pub fn to_string(&'_ self) -> Cow<'_, str> { + match self { + StringConstant::String(s) => s.as_str().into(), + StringConstant::Number(n) => n.to_string().into(), + } + } + + pub fn into_string(self) -> String { + match self { + StringConstant::String(s) => s, + StringConstant::Number(n) => n.to_string(), + } + } +} + +impl From for Value { + fn from(value: StringConstant) -> Self { + match value { + StringConstant::String(s) => Value::Text(s.into()), + StringConstant::Number(n) => Value::Number(n), + } + } +} + +impl Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::CurlyOpen => f.write_str("{"), + Token::CurlyClose => f.write_str("}"), + Token::BracketOpen => f.write_str("["), + Token::BracketClose => f.write_str("]"), + Token::ParenthesisOpen => f.write_str("("), + Token::ParenthesisClose => f.write_str(")"), + Token::Comma => f.write_str(","), + Token::Semicolon => f.write_str(";"), + Token::Colon => f.write_str(":"), + Token::Number(n) => write!(f, "{n}"), + Token::Identifier(w) => w.fmt(f), + Token::Tag(t) => write!(f, ":{t}"), + Token::Unknown(s) => f.write_str(s), + Token::StringVariable(s) => f.write_str(&String::from_utf8_lossy(s)), + Token::StringConstant(c) => match c { + StringConstant::String(s) => f.write_str(s), + StringConstant::Number(n) => write!(f, "{n}"), + }, + } + } +} diff --git a/vendor/sieve-rs/src/compiler/lexer/string.rs b/vendor/sieve-rs/src/compiler/lexer/string.rs new file mode 100644 index 0000000..3a2afa4 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/lexer/string.rs @@ -0,0 +1,922 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use mail_parser::HeaderName; + +use crate::{ + Envelope, MAX_MATCH_VARIABLES, + compiler::{ + ContentTypePart, ErrorType, HeaderPart, HeaderVariable, MessagePart, Number, + ReceivedHostname, ReceivedPart, Value, VariableType, + grammar::{ + AddressPart, + expr::{self}, + instruction::CompilerState, + }, + }, + runtime::eval::IntoString, +}; + +enum State { + None, + Variable, + Encoded { + is_unicode: bool, + initial_buf_size: usize, + }, +} + +impl CompilerState<'_> { + pub(crate) fn tokenize_string( + &mut self, + bytes: &[u8], + parse_decoded: bool, + ) -> Result { + let mut state = State::None; + let mut items = Vec::with_capacity(3); + let mut last_ch = 0; + + let mut var_start_pos = usize::MAX; + let mut var_is_number = true; + let mut var_has_namespace = false; + + let mut text_has_digits = true; + let mut text_has_dots = false; + + let mut hex_start = usize::MAX; + let mut decode_buf = Vec::with_capacity(bytes.len()); + + for (pos, &ch) in bytes.iter().enumerate() { + let mut is_var_error = false; + + match state { + State::None => match ch { + b'{' if last_ch == b'$' => { + decode_buf.pop(); + var_start_pos = pos + 1; + var_is_number = true; + var_has_namespace = false; + state = State::Variable; + } + b'.' => { + if text_has_dots { + text_has_digits = false; + } else { + text_has_dots = true; + } + decode_buf.push(ch); + } + b'0'..=b'9' => { + decode_buf.push(ch); + } + _ => { + text_has_digits = false; + decode_buf.push(ch); + } + }, + State::Variable => match ch { + b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'[' | b']' | b'*' | b'-' => { + var_is_number = false; + } + b'.' => { + var_is_number = false; + var_has_namespace = true; + } + b'0'..=b'9' => {} + b'}' if pos > var_start_pos => { + // Add any text before the variable + if !decode_buf.is_empty() { + self.add_value( + &mut items, + &decode_buf, + parse_decoded, + text_has_digits, + text_has_dots, + )?; + decode_buf.clear(); + text_has_digits = true; + text_has_dots = false; + } + + // Parse variable type + let var_name = std::str::from_utf8(&bytes[var_start_pos..pos]).unwrap(); + let var_type = if !var_is_number { + self.parse_variable(var_name, var_has_namespace) + } else { + self.parse_match_variable(var_name) + }; + + match var_type { + Ok(Some(var)) => items.push(Value::Variable(var)), + Ok(None) => {} + Err(ErrorType::InvalidNamespace(_) | ErrorType::InvalidEnvelope(_)) => { + is_var_error = true; + } + Err(e) => return Err(e), + } + + state = State::None; + } + b':' => { + if parse_decoded && !var_has_namespace { + match bytes.get(var_start_pos..pos) { + Some(enc) if enc.eq_ignore_ascii_case(b"hex") => { + state = State::Encoded { + is_unicode: false, + initial_buf_size: decode_buf.len(), + }; + } + Some(enc) if enc.eq_ignore_ascii_case(b"unicode") => { + state = State::Encoded { + is_unicode: true, + initial_buf_size: decode_buf.len(), + }; + } + _ => { + is_var_error = true; + } + } + } else if var_has_namespace { + var_is_number = false; + } else { + is_var_error = true; + } + } + _ => { + is_var_error = true; + } + }, + + State::Encoded { + is_unicode, + initial_buf_size, + } => match ch { + b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => { + if hex_start == usize::MAX { + hex_start = pos; + } + } + b' ' | b'\t' | b'\r' | b'\n' | b'}' => { + if hex_start != usize::MAX { + let code = std::str::from_utf8(&bytes[hex_start..pos]).unwrap(); + hex_start = usize::MAX; + + if !is_unicode { + if let Ok(ch) = u8::from_str_radix(code, 16) { + decode_buf.push(ch); + } else { + is_var_error = true; + } + } else if let Ok(ch) = u32::from_str_radix(code, 16) { + let mut buf = [0; 4]; + decode_buf.extend_from_slice( + char::from_u32(ch) + .ok_or(ErrorType::InvalidUnicodeSequence(ch))? + .encode_utf8(&mut buf) + .as_bytes(), + ); + } else { + is_var_error = true; + } + } + if ch == b'}' { + if decode_buf.len() != initial_buf_size { + state = State::None; + } else { + is_var_error = true; + } + } + } + _ => { + is_var_error = true; + } + }, + } + + if is_var_error { + if let State::Encoded { + initial_buf_size, .. + } = state + && initial_buf_size != decode_buf.len() + { + decode_buf.truncate(initial_buf_size); + } + decode_buf.extend_from_slice(&bytes[var_start_pos - 2..pos + 1]); + hex_start = usize::MAX; + state = State::None; + } + + last_ch = ch; + } + + match state { + State::Variable => { + decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]); + } + State::Encoded { + initial_buf_size, .. + } => { + if initial_buf_size != decode_buf.len() { + decode_buf.truncate(initial_buf_size); + } + decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]); + } + State::None => (), + } + + if !decode_buf.is_empty() { + self.add_value( + &mut items, + &decode_buf, + parse_decoded, + text_has_digits, + text_has_dots, + )?; + } + + Ok(match items.len() { + 1 => items.pop().unwrap(), + 0 => Value::Text(String::new().into()), + _ => Value::List(items), + }) + } + + fn parse_match_variable(&mut self, var_name: &str) -> Result, ErrorType> { + let num = var_name + .parse() + .map_err(|_| ErrorType::InvalidNumber(var_name.to_string()))?; + if num < MAX_MATCH_VARIABLES as usize { + if self.register_match_var(num) { + let total_vars = num + 1; + if total_vars > self.vars_match_max { + self.vars_match_max = total_vars; + } + Ok(Some(VariableType::Match(num))) + } else { + Ok(None) + } + } else { + Err(ErrorType::InvalidMatchVariable(num)) + } + } + + pub fn parse_variable( + &self, + var_name: &str, + maybe_namespace: bool, + ) -> Result, ErrorType> { + if !maybe_namespace { + if self.is_var_global(var_name) { + Ok(Some(VariableType::Global(var_name.to_string()))) + } else if let Some(var_id) = self.get_local_var(var_name) { + Ok(Some(VariableType::Local(var_id))) + } else { + Ok(None) + } + } else { + let var = match var_name.to_lowercase().split_once('.') { + Some(("global" | "t", var_name)) if !var_name.is_empty() => { + VariableType::Global(var_name.to_string()) + } + Some(("env", var_name)) if !var_name.is_empty() => { + VariableType::Environment(var_name.to_string()) + } + Some(("envelope", var_name)) if !var_name.is_empty() => { + let envelope = match var_name { + "from" => Envelope::From, + "to" => Envelope::To, + "by_time_absolute" => Envelope::ByTimeAbsolute, + "by_time_relative" => Envelope::ByTimeRelative, + "by_mode" => Envelope::ByMode, + "by_trace" => Envelope::ByTrace, + "notify" => Envelope::Notify, + "orcpt" => Envelope::Orcpt, + "ret" => Envelope::Ret, + "envid" => Envelope::Envid, + _ => { + return Err(ErrorType::InvalidEnvelope(var_name.to_string())); + } + }; + VariableType::Envelope(envelope) + } + Some(("header", var_name)) if !var_name.is_empty() => { + self.parse_header_variable(var_name)? + } + Some(("body", var_name)) if !var_name.is_empty() => match var_name { + "text" => VariableType::Part(MessagePart::TextBody(false)), + "html" => VariableType::Part(MessagePart::HtmlBody(false)), + "to_text" => VariableType::Part(MessagePart::TextBody(true)), + "to_html" => VariableType::Part(MessagePart::HtmlBody(true)), + _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())), + }, + Some(("part", var_name)) if !var_name.is_empty() => match var_name { + "text" => VariableType::Part(MessagePart::Contents), + "raw" => VariableType::Part(MessagePart::Raw), + _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())), + }, + None => { + if self.is_var_global(var_name) { + VariableType::Global(var_name.to_string()) + } else if let Some(var_id) = self.get_local_var(var_name) { + VariableType::Local(var_id) + } else { + return Ok(None); + } + } + _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())), + }; + + Ok(Some(var)) + } + } + + fn parse_header_variable(&self, var_name: &str) -> Result { + #[derive(Debug)] + enum State { + Name, + Index, + Part, + PartIndex, + } + let mut name = vec![]; + let mut has_name = false; + let mut has_wildcard = false; + let mut hdr_name = String::new(); + let mut hdr_index = String::new(); + let mut part = String::new(); + let mut part_index = String::new(); + let mut state = State::Name; + + for ch in var_name.chars() { + match state { + State::Name => match ch { + '[' => { + state = if hdr_index.is_empty() { + State::Index + } else if part.is_empty() { + State::PartIndex + } else { + return Err(ErrorType::InvalidExpression(var_name.to_string())); + }; + has_name = true; + } + '.' => { + state = State::Part; + has_name = true; + } + ' ' | '\t' | '\r' | '\n' => {} + '*' if !has_wildcard && hdr_name.is_empty() && name.is_empty() => { + has_wildcard = true; + } + ':' if !hdr_name.is_empty() && !has_wildcard => { + name.push( + HeaderName::parse(std::mem::take(&mut hdr_name)).ok_or_else(|| { + ErrorType::InvalidExpression(var_name.to_string()) + })?, + ); + } + _ if !has_name && !has_wildcard => { + hdr_name.push(ch); + } + _ => { + return Err(ErrorType::InvalidExpression(var_name.to_string())); + } + }, + State::Index => match ch { + ']' => { + state = State::Name; + } + ' ' | '\t' | '\r' | '\n' => {} + _ => { + hdr_index.push(ch); + } + }, + State::Part => match ch { + '[' => { + state = State::PartIndex; + } + ' ' | '\t' | '\r' | '\n' => {} + _ => { + part.push(ch); + } + }, + State::PartIndex => match ch { + ']' => { + state = State::Name; + } + ' ' | '\t' | '\r' | '\n' => {} + _ => { + part_index.push(ch); + } + }, + } + } + + if !hdr_name.is_empty() { + name.push( + HeaderName::parse(hdr_name) + .ok_or_else(|| ErrorType::InvalidExpression(var_name.to_string()))?, + ); + } + + if !name.is_empty() || has_wildcard { + Ok(VariableType::Header(HeaderVariable { + name, + part: HeaderPart::try_from(part.as_str()) + .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?, + index_hdr: match hdr_index.as_str() { + "" => { + if !has_wildcard { + -1 + } else { + 0 + } + } + "*" => 0, + _ => hdr_index + .parse() + .map(|v| if v == 0 { 1 } else { v }) + .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?, + }, + index_part: match part_index.as_str() { + "" => { + if !has_wildcard { + -1 + } else { + 0 + } + } + "*" => 0, + _ => part_index + .parse() + .map(|v| if v == 0 { 1 } else { v }) + .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?, + }, + })) + } else { + Err(ErrorType::InvalidExpression(var_name.to_string())) + } + } + + pub fn parse_expr_fnc_or_var( + &self, + var_name: &str, + maybe_namespace: bool, + ) -> Result { + match self.parse_variable(var_name, maybe_namespace) { + Ok(Some(var)) => Ok(expr::Token::Variable(var)), + _ => { + if let Some((id, num_args)) = self.compiler.functions.get(var_name) { + Ok(expr::Token::Function { + name: var_name.to_string(), + id: *id, + num_args: *num_args, + }) + } else { + Err(format!("Invalid variable or function name {var_name:?}")) + } + } + } + } + + #[inline(always)] + fn add_value( + &mut self, + items: &mut Vec, + buf: &[u8], + parse_decoded: bool, + has_digits: bool, + has_dots: bool, + ) -> Result<(), ErrorType> { + if !parse_decoded { + items.push(if has_digits { + if has_dots { + match std::str::from_utf8(buf) + .ok() + .and_then(|v| (v, v.parse::().ok()?).into()) + { + Some((v, n)) if n.to_string() == v => Value::Number(Number::Float(n)), + _ => Value::Text(buf.to_vec().into_string().into()), + } + } else { + match std::str::from_utf8(buf) + .ok() + .and_then(|v| (v, v.parse::().ok()?).into()) + { + Some((v, n)) if n.to_string() == v => Value::Number(Number::Integer(n)), + _ => Value::Text(buf.to_vec().into_string().into()), + } + } + } else { + Value::Text(buf.to_vec().into_string().into()) + }); + } else { + match self.tokenize_string(buf, false)? { + Value::List(new_items) => items.extend(new_items), + item => items.push(item), + } + } + + Ok(()) + } +} + +impl TryFrom<&str> for HeaderPart { + type Error = (); + + fn try_from(value: &str) -> Result { + let (value, subvalue) = value.split_once('.').unwrap_or((value, "")); + Ok(match value { + "" | "text" => HeaderPart::Text, + // Addresses + "name" => HeaderPart::Address(AddressPart::Name), + "addr" => { + if !subvalue.is_empty() { + HeaderPart::Address(AddressPart::try_from(subvalue)?) + } else { + HeaderPart::Address(AddressPart::All) + } + } + + // Content-type + "type" => HeaderPart::ContentType(ContentTypePart::Type), + "subtype" => HeaderPart::ContentType(ContentTypePart::Subtype), + "attr" if !subvalue.is_empty() => { + HeaderPart::ContentType(ContentTypePart::Attribute(subvalue.to_string())) + } + + // Received + "rcvd" => { + if !subvalue.is_empty() { + HeaderPart::Received(ReceivedPart::try_from(subvalue)?) + } else { + HeaderPart::Text + } + } + + // Id + "id" => HeaderPart::Id, + + // Raw + "raw" => HeaderPart::Raw, + "raw_name" => HeaderPart::RawName, + + // Date + "date" => HeaderPart::Date, + + // Exists + "exists" => HeaderPart::Exists, + + _ => { + return Err(()); + } + }) + } +} + +impl TryFrom<&str> for ReceivedPart { + type Error = (); + + fn try_from(value: &str) -> Result { + Ok(match value { + // Received + "from" => ReceivedPart::From(ReceivedHostname::Any), + "from.name" => ReceivedPart::From(ReceivedHostname::Name), + "from.ip" => ReceivedPart::From(ReceivedHostname::Ip), + "ip" => ReceivedPart::FromIp, + "iprev" => ReceivedPart::FromIpRev, + "by" => ReceivedPart::By(ReceivedHostname::Any), + "by.name" => ReceivedPart::By(ReceivedHostname::Name), + "by.ip" => ReceivedPart::By(ReceivedHostname::Ip), + "for" => ReceivedPart::For, + "with" => ReceivedPart::With, + "tls" => ReceivedPart::TlsVersion, + "cipher" => ReceivedPart::TlsCipher, + "id" => ReceivedPart::Id, + "ident" => ReceivedPart::Ident, + "date" => ReceivedPart::Date, + "date.raw" => ReceivedPart::DateRaw, + _ => return Err(()), + }) + } +} + +impl TryFrom<&str> for AddressPart { + type Error = (); + + fn try_from(value: &str) -> Result { + Ok(match value { + "name" => AddressPart::Name, + "addr" | "all" => AddressPart::All, + "addr.domain" => AddressPart::Domain, + "addr.local" => AddressPart::LocalPart, + "addr.user" => AddressPart::User, + "addr.detail" => AddressPart::Detail, + _ => return Err(()), + }) + } +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Text(t) => f.write_str(t), + Value::List(l) => { + for i in l { + i.fmt(f)?; + } + Ok(()) + } + Value::Number(n) => n.fmt(f), + Value::Variable(v) => v.fmt(f), + Value::Regex(r) => f.write_str(&r.expr), + } + } +} + +impl Display for VariableType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VariableType::Local(v) => write!(f, "${{{v}}}"), + VariableType::Match(v) => write!(f, "${{{v}}}"), + VariableType::Global(v) => write!(f, "${{global.{v}}}"), + VariableType::Environment(v) => write!(f, "${{env.{v}}}"), + + VariableType::Envelope(env) => f.write_str(match env { + Envelope::From => "${{envelope.from}}", + Envelope::To => "${{envelope.to}}", + Envelope::ByTimeAbsolute => "${{envelope.by_time_absolute}}", + Envelope::ByTimeRelative => "${{envelope.by_time_relative}}", + Envelope::ByMode => "${{envelope.by_mode}}", + Envelope::ByTrace => "${{envelope.by_trace}}", + Envelope::Notify => "${{envelope.notify}}", + Envelope::Orcpt => "${{envelope.orcpt}}", + Envelope::Ret => "${{envelope.ret}}", + Envelope::Envid => "${{envelope.envit}}", + }), + + VariableType::Header(hdr) => { + write!( + f, + "${{header.{}", + hdr.name.first().map(|h| h.as_str()).unwrap_or_default() + )?; + if hdr.index_hdr != 0 { + write!(f, "[{}]", hdr.index_hdr)?; + } else { + f.write_str("[*]")?; + } + /*if hdr.part != HeaderPart::Text { + f.write_str(".")?; + f.write_str(match &hdr.part { + HeaderPart::Name => "name", + HeaderPart::Address => "address", + HeaderPart::Type => "type", + HeaderPart::Subtype => "subtype", + HeaderPart::Raw => "raw", + HeaderPart::Date => "date", + HeaderPart::Attribute(attr) => attr.as_str(), + HeaderPart::Text => unreachable!(), + })?; + }*/ + if hdr.index_part != 0 { + write!(f, "[{}]", hdr.index_part)?; + } else { + f.write_str("[*]")?; + } + f.write_str("}") + } + VariableType::Part(part) => { + write!( + f, + "${{{}", + match part { + MessagePart::TextBody(true) => "body.to_text", + MessagePart::TextBody(false) => "body.text", + MessagePart::HtmlBody(true) => "body.to_html", + MessagePart::HtmlBody(false) => "body.html", + MessagePart::Contents => "part.text", + MessagePart::Raw => "part.raw", + } + )?; + f.write_str("}") + } + } + } +} + +#[cfg(test)] +mod tests { + + use mail_parser::HeaderName; + + use super::Value; + use crate::compiler::grammar::instruction::{Block, CompilerState, Instruction, MAX_PARAMS}; + use crate::compiler::grammar::test::Test; + use crate::compiler::grammar::tests::test_string::TestString; + use crate::compiler::grammar::{Comparator, MatchType}; + use crate::compiler::lexer::tokenizer::Tokenizer; + use crate::compiler::lexer::word::Word; + use crate::compiler::{AddressPart, HeaderPart, HeaderVariable, VariableType}; + use crate::{AHashSet, Compiler}; + + #[test] + fn tokenize_string() { + let c = Compiler::new(); + let mut block = Block::new(Word::Not); + block.match_test_pos.push(0); + let mut compiler = CompilerState { + compiler: &c, + instructions: vec![Instruction::Test(Test::String(TestString { + match_type: MatchType::Regex(u64::MAX), + comparator: Comparator::AsciiCaseMap, + source: vec![Value::Variable(VariableType::Local(0))], + key_list: vec![Value::Variable(VariableType::Local(0))], + is_not: false, + }))], + block_stack: Vec::new(), + block, + last_block_type: Word::Not, + vars_global: AHashSet::new(), + vars_num: 0, + vars_num_max: 0, + vars_local: 0, + tokens: Tokenizer::new(&c, b""), + vars_match_max: usize::MAX, + param_check: [false; MAX_PARAMS], + includes_num: 0, + }; + + for (input, expected_result) in [ + ("$${hex:24 24}", Value::Text("$$$".to_string().into())), + ("$${hex:40}", Value::Text("$@".to_string().into())), + ("${hex: 40 }", Value::Text("@".to_string().into())), + ("${HEX: 40}", Value::Text("@".to_string().into())), + ("${hex:40", Value::Text("${hex:40".to_string().into())), + ("${hex:400}", Value::Text("${hex:400}".to_string().into())), + ( + "${hex:4${hex:30}}", + Value::Text("${hex:40}".to_string().into()), + ), + ("${unicode:40}", Value::Text("@".to_string().into())), + ( + "${ unicode:40}", + Value::Text("${ unicode:40}".to_string().into()), + ), + ("${UNICODE:40}", Value::Text("@".to_string().into())), + ("${UnICoDE:0000040}", Value::Text("@".to_string().into())), + ("${Unicode:40}", Value::Text("@".to_string().into())), + ( + "${Unicode:40 40 ", + Value::Text("${Unicode:40 40 ".to_string().into()), + ), + ( + "${Unicode:Cool}", + Value::Text("${Unicode:Cool}".to_string().into()), + ), + ("", Value::Text("".to_string().into())), + ( + "${global.full}", + Value::Variable(VariableType::Global("full".to_string())), + ), + ( + "${BAD${global.Company}", + Value::List(vec![ + Value::Text("${BAD".to_string().into()), + Value::Variable(VariableType::Global("company".to_string())), + ]), + ), + ( + "${President, ${global.Company} Inc.}", + Value::List(vec![ + Value::Text("${President, ".to_string().into()), + Value::Variable(VariableType::Global("company".to_string())), + Value::Text(" Inc.}".to_string().into()), + ]), + ), + ( + "dear${hex:20 24 7b}global.Name}", + Value::List(vec![ + Value::Text("dear ".to_string().into()), + Value::Variable(VariableType::Global("name".to_string())), + ]), + ), + ( + "INBOX.lists.${2}", + Value::List(vec![ + Value::Text("INBOX.lists.".to_string().into()), + Value::Variable(VariableType::Match(2)), + ]), + ), + ( + "Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test", + Value::Text("Ein unerhört großer Test".to_string().into()), + ), + ("&%${}!", Value::Text("&%${}!".to_string().into())), + ("${doh!}", Value::Text("${doh!}".to_string().into())), + ( + "${hex: 20 }${global.hi}${hex: 20 }", + Value::List(vec![ + Value::Text(" ".to_string().into()), + Value::Variable(VariableType::Global("hi".to_string())), + Value::Text(" ".to_string().into()), + ]), + ), + ( + "${hex:20 24 7b z}${global.hi}${unicode:}${unicode: }${hex:20}", + Value::List(vec![ + Value::Text("${hex:20 24 7b z}".to_string().into()), + Value::Variable(VariableType::Global("hi".to_string())), + Value::Text("${unicode:}${unicode: } ".to_string().into()), + ]), + ), + ( + "${header.from}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Text, + index_hdr: -1, + index_part: -1, + })), + ), + ( + "${header.from.addr}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::All), + index_hdr: -1, + index_part: -1, + })), + ), + ( + "${header.from[1]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Text, + index_hdr: 1, + index_part: -1, + })), + ), + ( + "${header.from[*]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Text, + index_hdr: 0, + index_part: -1, + })), + ), + ( + "${header.from[20].name}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::Name), + index_hdr: 20, + index_part: -1, + })), + ), + ( + "${header.from[*].addr}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::All), + index_hdr: 0, + index_part: -1, + })), + ), + ( + "${header.from[-5].name[2]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::Name), + index_hdr: -5, + index_part: 2, + })), + ), + ( + "${header.from[*].raw[*]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Raw, + index_hdr: 0, + index_part: 0, + })), + ), + ] { + assert_eq!( + compiler.tokenize_string(input.as_bytes(), true).unwrap(), + expected_result, + "Failed for {input}" + ); + } + + for input in ["${unicode:200000}", "${Unicode:DF01}"] { + assert!(compiler.tokenize_string(input.as_bytes(), true).is_err()); + } + } +} diff --git a/vendor/sieve-rs/src/compiler/lexer/tokenizer.rs b/vendor/sieve-rs/src/compiler/lexer/tokenizer.rs new file mode 100644 index 0000000..d8698fa --- /dev/null +++ b/vendor/sieve-rs/src/compiler/lexer/tokenizer.rs @@ -0,0 +1,577 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use super::{StringConstant, Token, word::lookup_words}; +use crate::{ + Compiler, + compiler::{CompileError, ErrorType, Number}, + runtime::eval::IntoString, +}; +use std::{iter::Peekable, slice::Iter}; + +pub(crate) struct Tokenizer<'x> { + pub compiler: &'x Compiler, + pub iter: Peekable>, + pub buf: Vec, + pub next_token: Vec, + + pub pos: usize, + pub line_num: usize, + pub line_start: usize, + + pub text_line_num: usize, + pub text_line_pos: usize, + + pub token_line_num: usize, + pub token_line_pos: usize, + + pub token_is_tag: bool, + + pub last_ch: u8, + pub state: State, +} + +#[derive(Debug)] +pub(crate) struct TokenInfo { + pub(crate) token: Token, + pub(crate) line_num: usize, + pub(crate) line_pos: usize, +} + +pub(crate) enum State { + None, + BracketComment, + HashComment, + QuotedString(StringType), + MultiLine(StringType), +} + +#[derive(Clone, Copy, Default)] +pub(crate) struct StringType { + maybe_variable: bool, + has_other: bool, + has_digits: bool, + has_dots: bool, +} + +impl<'x> Tokenizer<'x> { + pub fn new(compiler: &'x Compiler, bytes: &'x [u8]) -> Self { + Tokenizer { + compiler, + iter: bytes.iter().peekable(), + buf: Vec::with_capacity(bytes.len() / 2), + pos: usize::MAX, + line_num: 1, + line_start: 0, + text_line_num: 0, + text_line_pos: 0, + token_line_num: 0, + token_line_pos: 0, + token_is_tag: false, + next_token: Vec::with_capacity(2), + last_ch: 0, + state: State::None, + } + } + + pub fn get_current_token(&mut self) -> Option { + if !self.buf.is_empty() { + let word = std::str::from_utf8(&self.buf).unwrap(); + let token = if let Some(word) = lookup_words(word) { + if self.token_is_tag { + self.token_line_pos -= 1; + Token::Tag(word) + } else { + Token::Identifier(word) + } + } else if self.buf.first().unwrap().is_ascii_digit() { + let multiplier = match self.buf.last().unwrap() { + b'k' => 1024, + b'm' => 1048576, + b'g' => 1073741824, + _ => 1, + }; + + if let Ok(number) = (if multiplier > 1 && self.buf.len() > 1 { + std::str::from_utf8(&self.buf[..self.buf.len() - 1]).unwrap() + } else { + word + }) + .parse::() + { + Token::Number(number.saturating_mul(multiplier)) + } else if self.token_is_tag { + Token::Unknown(format!(":{word}")) + } else { + Token::Unknown(word.to_string()) + } + } else if self.token_is_tag { + Token::Unknown(format!(":{word}")) + } else { + Token::Unknown(word.to_string()) + }; + + self.reset_current_token(); + + Some(TokenInfo { + token, + line_num: self.token_line_num, + line_pos: self.token_line_pos, + }) + } else { + None + } + } + + #[inline(always)] + pub fn reset_current_token(&mut self) { + self.buf.clear(); + self.token_is_tag = false; + } + + #[inline(always)] + pub fn token_is_tag(&mut self) { + self.token_is_tag = true; + } + + pub fn get_token(&mut self, token: Token) -> TokenInfo { + let next_token = TokenInfo { + token, + line_num: self.line_num, + line_pos: self.pos - self.line_start, + }; + if let Some(token) = self.get_current_token() { + self.next_token.push(next_token); + token + } else { + next_token + } + } + + pub fn get_string(&mut self, str_type: StringType) -> Result { + if self.buf.len() < self.compiler.max_string_size { + let token = if str_type.maybe_variable { + Token::StringVariable(self.buf.to_vec()) + } else { + let constant = self.buf.to_vec().into_string(); + if !str_type.has_other && str_type.has_digits { + if !str_type.has_dots { + if let Some(number) = constant + .parse::() + .ok() + .filter(|&n| n.to_string() == constant) + { + Token::StringConstant(StringConstant::Number(Number::Integer(number))) + } else { + Token::StringConstant(StringConstant::String(constant)) + } + } else if let Some(number) = constant + .parse::() + .ok() + .filter(|&n| n.to_string() == constant) + { + Token::StringConstant(StringConstant::Number(Number::Float(number))) + } else { + Token::StringConstant(StringConstant::String(constant)) + } + } else { + Token::StringConstant(StringConstant::String(constant)) + } + }; + + self.buf.clear(); + + Ok(TokenInfo { + token, + line_num: self.text_line_num, + line_pos: self.text_line_pos, + }) + } else { + Err(CompileError { + line_num: self.text_line_num, + line_pos: self.text_line_pos, + error_type: ErrorType::StringTooLong, + }) + } + } + + #[inline(always)] + pub fn push_byte(&mut self, ch: u8) { + if self.buf.is_empty() { + self.token_line_num = self.line_num; + self.token_line_pos = self.pos - self.line_start; + } + self.buf.push(ch); + } + + #[inline(always)] + pub fn new_line(&mut self) { + self.line_num += 1; + self.line_start = self.pos; + } + + #[inline(always)] + pub fn text_start(&mut self) { + self.text_line_num = self.line_num; + self.text_line_pos = self.pos - self.line_start; + } + + #[inline(always)] + pub fn is_token_start(&self) -> bool { + self.buf.is_empty() + } + + #[inline(always)] + pub fn token_bytes(&self) -> &[u8] { + &self.buf + } + + #[inline(always)] + pub fn next_byte(&mut self) -> Option<(u8, u8)> { + self.iter.next().map(|&ch| { + let last_ch = self.last_ch; + self.pos = self.pos.wrapping_add(1); + self.last_ch = ch; + (ch, last_ch) + }) + } + + #[inline(always)] + pub fn peek_byte(&mut self) -> Option { + self.iter.peek().map(|ch| **ch) + } + + pub fn unwrap_next(&mut self) -> Result { + if let Some(token) = self.next() { + token + } else { + Err(CompileError { + line_num: self.line_num, + line_pos: self.pos - self.line_start, + error_type: ErrorType::UnexpectedEOF, + }) + } + } + + pub fn expect_token(&mut self, token: Token) -> Result<(), CompileError> { + let next_token = self.unwrap_next()?; + if next_token.token == token { + Ok(()) + } else { + Err(next_token.expected(format!("'{token}'"))) + } + } + + pub fn expect_static_string(&mut self) -> Result { + let next_token = self.unwrap_next()?; + match next_token.token { + Token::StringConstant(s) => Ok(s.into_string()), + Token::BracketOpen => { + let mut string = None; + loop { + let token_info = self.unwrap_next()?; + match token_info.token { + Token::StringConstant(string_) => { + string = string_.into(); + } + Token::BracketClose if string.is_some() => break, + _ => return Err(token_info.expected("constant string")), + } + } + Ok(string.unwrap().into_string()) + } + _ => Err(next_token.expected("constant string")), + } + } + + pub fn expect_number(&mut self, max_value: usize) -> Result { + let next_token = self.unwrap_next()?; + if let Token::Number(n) = next_token.token { + if n < max_value { + Ok(n) + } else { + Err(next_token.expected(format!("number lower than {max_value}"))) + } + } else { + Err(next_token.expected("number")) + } + } + + pub fn invalid_character(&self) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.pos - self.line_start, + error_type: ErrorType::InvalidCharacter(self.last_ch), + } + } + + pub fn peek(&mut self) -> Option> { + if self.next_token.is_empty() { + match self.next()? { + Ok(next_token) => self.next_token.push(next_token), + Err(err) => return Some(Err(err)), + } + } + self.next_token.last().map(Ok) + } +} + +impl Iterator for Tokenizer<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + if let Some(prev_token) = self.next_token.pop() { + return Some(Ok(prev_token)); + } + + 'outer: while let Some((ch, last_ch)) = self.next_byte() { + match self.state { + State::None => match ch { + b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'$' => { + self.push_byte(ch); + } + b'A'..=b'Z' => { + self.push_byte(ch.to_ascii_lowercase()); + } + b':' => { + if self.is_token_start() + && matches!(self.peek_byte(), Some(b) if b.is_ascii_alphabetic()) + { + self.token_is_tag(); + } else if self.token_bytes().eq_ignore_ascii_case(b"text") { + self.state = State::MultiLine(StringType::default()); + self.text_start(); + while let Some((ch, _)) = self.next_byte() { + if ch == b'\n' { + self.new_line(); + self.reset_current_token(); + continue 'outer; + } + } + } else { + return Some(Ok(self.get_token(Token::Colon))); + //return Some(Err(self.invalid_character())); + } + } + b'"' => { + self.state = State::QuotedString(StringType::default()); + self.text_start(); + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + b'{' => { + return Some(Ok(self.get_token(Token::CurlyOpen))); + } + b'}' => { + return Some(Ok(self.get_token(Token::CurlyClose))); + } + b';' => { + return Some(Ok(self.get_token(Token::Semicolon))); + } + b',' => { + return Some(Ok(self.get_token(Token::Comma))); + } + b'[' => { + return Some(Ok(self.get_token(Token::BracketOpen))); + } + b']' => { + return Some(Ok(self.get_token(Token::BracketClose))); + } + b'(' => { + return Some(Ok(self.get_token(Token::ParenthesisOpen))); + } + b')' => { + return Some(Ok(self.get_token(Token::ParenthesisClose))); + } + b'/' => { + if let Some((b'*', _)) = self.next_byte() { + self.last_ch = 0; + self.state = State::BracketComment; + self.text_start(); + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } else { + return Some(Err(self.invalid_character())); + } + } + b'#' => { + self.state = State::HashComment; + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + b'\n' => { + self.new_line(); + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + b' ' | b'\t' | b'\r' => { + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + _ => { + return Some(Err(self.invalid_character())); + } + }, + State::BracketComment => match ch { + b'/' if last_ch == b'*' => { + self.state = State::None; + } + b'\n' => { + self.new_line(); + } + _ => (), + }, + State::HashComment => { + if ch == b'\n' { + self.state = State::None; + self.new_line(); + } + } + State::QuotedString(mut str_type) => match ch { + b'"' if last_ch != b'\\' => { + self.state = State::None; + return Some(self.get_string(str_type)); + } + b'\n' => { + self.new_line(); + self.push_byte(b'\n'); + str_type.has_other = true; + self.state = State::QuotedString(str_type); + } + b'{' if (last_ch == b'$' || last_ch == b'%') => { + str_type.maybe_variable = true; + self.state = State::QuotedString(str_type); + self.push_byte(ch); + } + b'\\' => { + if last_ch == b'\\' { + self.push_byte(ch); + } + } + b'0'..=b'9' => { + if !str_type.has_digits { + str_type.has_digits = true; + self.state = State::QuotedString(str_type); + } + self.push_byte(ch); + } + b'.' => { + if !str_type.has_dots { + str_type.has_dots = true; + } else { + str_type.has_other = true; + } + self.state = State::QuotedString(str_type); + self.push_byte(ch); + } + _ => { + let ch = if last_ch == b'\\' { + match ch { + b'n' => b'\n', + b'r' => b'\r', + b't' => b'\t', + _ => ch, + } + } else { + ch + }; + if !str_type.has_other && ch != b'-' { + str_type.has_other = true; + self.state = State::QuotedString(str_type); + } + self.push_byte(ch); + } + }, + State::MultiLine(mut str_type) => match ch { + b'.' if last_ch == b'\n' => { + let is_eof = match (self.next_byte(), self.peek_byte()) { + (Some((b'\r', _)), Some(b'\n')) => { + self.next_byte(); + true + } + (Some((b'\n', _)), _) => true, + (Some((b'.', _)), _) => { + self.push_byte(b'.'); + false + } + (Some((ch, _)), _) => { + self.push_byte(b'.'); + self.push_byte(ch); + false + } + _ => false, + }; + + if is_eof { + self.new_line(); + self.state = State::None; + return Some(self.get_string(str_type)); + } + } + b'\n' => { + self.new_line(); + self.push_byte(b'\n'); + } + b'{' if (last_ch == b'$' || last_ch == b'%') => { + str_type.maybe_variable = true; + self.state = State::MultiLine(str_type); + self.push_byte(ch); + } + b'0'..=b'9' => { + if !str_type.has_digits { + str_type.has_digits = true; + self.state = State::MultiLine(str_type); + } + self.push_byte(ch); + } + b'.' => { + if !str_type.has_dots { + str_type.has_dots = true; + } else { + str_type.has_other = true; + } + self.state = State::MultiLine(str_type); + self.push_byte(ch); + } + _ => { + if !str_type.has_other && ch != b'-' { + str_type.has_other = true; + self.state = State::MultiLine(str_type); + } + self.push_byte(ch); + } + }, + } + } + + match self.state { + State::BracketComment | State::QuotedString(_) | State::MultiLine(_) => { + Some(Err(CompileError { + line_num: self.text_line_num, + line_pos: self.text_line_pos, + error_type: (&self.state).into(), + })) + } + _ => None, + } + } +} + +impl From<&State> for ErrorType { + fn from(state: &State) -> Self { + match state { + State::BracketComment => ErrorType::UnterminatedComment, + State::QuotedString(_) => ErrorType::UnterminatedString, + State::MultiLine(_) => ErrorType::UnterminatedMultiline, + _ => unreachable!(), + } + } +} diff --git a/vendor/sieve-rs/src/compiler/lexer/word.rs b/vendor/sieve-rs/src/compiler/lexer/word.rs new file mode 100644 index 0000000..d612748 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/lexer/word.rs @@ -0,0 +1,410 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum Word { + AddFlag, + AddHeader, + Address, + Addresses, + All, + AllOf, + AnyChild, + AnyOf, + Body, + Break, + ByMode, + ByTimeAbsolute, + ByTimeRelative, + ByTrace, + Comparator, + Contains, + Content, + ContentType, + Convert, + Copy, + Count, + Create, + CurrentDate, + Date, + Days, + DeleteHeader, + Detail, + Discard, + Domain, + Duplicate, + Else, + ElsIf, + Enclose, + EncodeUrl, + Envelope, + Environment, + Ereject, + Error, + Exists, + ExtractText, + False, + Fcc, + FileInto, + First, + Flags, + ForEveryPart, + From, + Global, + Handle, + HasFlag, + Header, + Headers, + If, + Ihave, + Importance, + Include, + Index, + Is, + Keep, + Last, + Length, + List, + LocalPart, + Lower, + LowerFirst, + MailboxExists, + MailboxId, + MailboxIdExists, + Matches, + Message, + Metadata, + MetadataExists, + Mime, + Name, + Not, + Notify, + NotifyMethodCapability, + Once, + Optional, + Options, + OriginalZone, + Over, + Param, + Percent, + Personal, + QuoteRegex, + QuoteWildcard, + Raw, + Redirect, + Regex, + Reject, + RemoveFlag, + Replace, + Require, + Ret, + Return, + Seconds, + ServerMetadata, + ServerMetadataExists, + Set, + SetFlag, + Size, + SpamTest, + SpecialUse, + SpecialUseExists, + Stop, + String, + Subject, + Subtype, + Text, + True, + Type, + Under, + UniqueId, + Upper, + UpperFirst, + User, + Vacation, + ValidExtList, + ValidNotifyMethod, + Value, + VirusTest, + Zone, + + // Extensions + Eval, + Local, + While, + Let, + Continue, +} + +pub(crate) fn lookup_words(input: &str) -> Option { + hashify::tiny_map!( + input.as_bytes(), + "addflag" => Word::AddFlag, + "addheader" => Word::AddHeader, + "address" => Word::Address, + "addresses" => Word::Addresses, + "all" => Word::All, + "allof" => Word::AllOf, + "anychild" => Word::AnyChild, + "anyof" => Word::AnyOf, + "body" => Word::Body, + "break" => Word::Break, + "bymode" => Word::ByMode, + "bytimeabsolute" => Word::ByTimeAbsolute, + "bytimerelative" => Word::ByTimeRelative, + "bytrace" => Word::ByTrace, + "comparator" => Word::Comparator, + "contains" => Word::Contains, + "content" => Word::Content, + "contenttype" => Word::ContentType, + "convert" => Word::Convert, + "copy" => Word::Copy, + "count" => Word::Count, + "create" => Word::Create, + "currentdate" => Word::CurrentDate, + "date" => Word::Date, + "days" => Word::Days, + "deleteheader" => Word::DeleteHeader, + "detail" => Word::Detail, + "discard" => Word::Discard, + "domain" => Word::Domain, + "duplicate" => Word::Duplicate, + "else" => Word::Else, + "elsif" => Word::ElsIf, + "enclose" => Word::Enclose, + "encodeurl" => Word::EncodeUrl, + "envelope" => Word::Envelope, + "environment" => Word::Environment, + "ereject" => Word::Ereject, + "error" => Word::Error, + "exists" => Word::Exists, + "extracttext" => Word::ExtractText, + "false" => Word::False, + "fcc" => Word::Fcc, + "fileinto" => Word::FileInto, + "first" => Word::First, + "flags" => Word::Flags, + "foreverypart" => Word::ForEveryPart, + "from" => Word::From, + "global" => Word::Global, + "handle" => Word::Handle, + "hasflag" => Word::HasFlag, + "header" => Word::Header, + "headers" => Word::Headers, + "if" => Word::If, + "ihave" => Word::Ihave, + "importance" => Word::Importance, + "include" => Word::Include, + "index" => Word::Index, + "is" => Word::Is, + "keep" => Word::Keep, + "last" => Word::Last, + "length" => Word::Length, + "list" => Word::List, + "localpart" => Word::LocalPart, + "lower" => Word::Lower, + "lowerfirst" => Word::LowerFirst, + "mailboxexists" => Word::MailboxExists, + "mailboxid" => Word::MailboxId, + "mailboxidexists" => Word::MailboxIdExists, + "matches" => Word::Matches, + "message" => Word::Message, + "metadata" => Word::Metadata, + "metadataexists" => Word::MetadataExists, + "mime" => Word::Mime, + "name" => Word::Name, + "not" => Word::Not, + "notify" => Word::Notify, + "notify_method_capability" => Word::NotifyMethodCapability, + "once" => Word::Once, + "optional" => Word::Optional, + "options" => Word::Options, + "originalzone" => Word::OriginalZone, + "over" => Word::Over, + "param" => Word::Param, + "percent" => Word::Percent, + "personal" => Word::Personal, + "quoteregex" => Word::QuoteRegex, + "quotewildcard" => Word::QuoteWildcard, + "raw" => Word::Raw, + "redirect" => Word::Redirect, + "regex" => Word::Regex, + "reject" => Word::Reject, + "removeflag" => Word::RemoveFlag, + "replace" => Word::Replace, + "require" => Word::Require, + "ret" => Word::Ret, + "return" => Word::Return, + "seconds" => Word::Seconds, + "servermetadata" => Word::ServerMetadata, + "servermetadataexists" => Word::ServerMetadataExists, + "set" => Word::Set, + "setflag" => Word::SetFlag, + "size" => Word::Size, + "spamtest" => Word::SpamTest, + "specialuse" => Word::SpecialUse, + "specialuse_exists" => Word::SpecialUseExists, + "stop" => Word::Stop, + "string" => Word::String, + "subject" => Word::Subject, + "subtype" => Word::Subtype, + "text" => Word::Text, + "true" => Word::True, + "type" => Word::Type, + "under" => Word::Under, + "uniqueid" => Word::UniqueId, + "upper" => Word::Upper, + "upperfirst" => Word::UpperFirst, + "user" => Word::User, + "vacation" => Word::Vacation, + "valid_ext_list" => Word::ValidExtList, + "valid_notify_method" => Word::ValidNotifyMethod, + "value" => Word::Value, + "virustest" => Word::VirusTest, + "zone" => Word::Zone, + "eval" => Word::Eval, + "local" => Word::Local, + "while" => Word::While, + "let" => Word::Let, + "continue" => Word::Continue, + ) +} + +impl Display for Word { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Word::AddFlag => f.write_str("addflag"), + Word::AddHeader => f.write_str("addheader"), + Word::Address => f.write_str("address"), + Word::Addresses => f.write_str("addresses"), + Word::All => f.write_str("all"), + Word::AllOf => f.write_str("allof"), + Word::AnyChild => f.write_str("anychild"), + Word::AnyOf => f.write_str("anyof"), + Word::Body => f.write_str("body"), + Word::Break => f.write_str("break"), + Word::ByMode => f.write_str("bymode"), + Word::ByTimeAbsolute => f.write_str("bytimeabsolute"), + Word::ByTimeRelative => f.write_str("bytimerelative"), + Word::ByTrace => f.write_str("bytrace"), + Word::Comparator => f.write_str("comparator"), + Word::Contains => f.write_str("contains"), + Word::Content => f.write_str("content"), + Word::ContentType => f.write_str("contenttype"), + Word::Convert => f.write_str("convert"), + Word::Copy => f.write_str("copy"), + Word::Count => f.write_str("count"), + Word::Create => f.write_str("create"), + Word::CurrentDate => f.write_str("currentdate"), + Word::Date => f.write_str("date"), + Word::Days => f.write_str("days"), + Word::DeleteHeader => f.write_str("deleteheader"), + Word::Detail => f.write_str("detail"), + Word::Discard => f.write_str("discard"), + Word::Domain => f.write_str("domain"), + Word::Duplicate => f.write_str("duplicate"), + Word::Else => f.write_str("else"), + Word::ElsIf => f.write_str("elsif"), + Word::Enclose => f.write_str("enclose"), + Word::EncodeUrl => f.write_str("encodeurl"), + Word::Envelope => f.write_str("envelope"), + Word::Environment => f.write_str("environment"), + Word::Ereject => f.write_str("ereject"), + Word::Error => f.write_str("error"), + Word::Exists => f.write_str("exists"), + Word::ExtractText => f.write_str("extracttext"), + Word::False => f.write_str("false"), + Word::Fcc => f.write_str("fcc"), + Word::FileInto => f.write_str("fileinto"), + Word::First => f.write_str("first"), + Word::Flags => f.write_str("flags"), + Word::ForEveryPart => f.write_str("foreverypart"), + Word::From => f.write_str("from"), + Word::Global => f.write_str("global"), + Word::Handle => f.write_str("handle"), + Word::HasFlag => f.write_str("hasflag"), + Word::Header => f.write_str("header"), + Word::Headers => f.write_str("headers"), + Word::If => f.write_str("if"), + Word::Ihave => f.write_str("ihave"), + Word::Importance => f.write_str("importance"), + Word::Include => f.write_str("include"), + Word::Index => f.write_str("index"), + Word::Is => f.write_str("is"), + Word::Keep => f.write_str("keep"), + Word::Last => f.write_str("last"), + Word::Length => f.write_str("length"), + Word::List => f.write_str("list"), + Word::LocalPart => f.write_str("localpart"), + Word::Lower => f.write_str("lower"), + Word::LowerFirst => f.write_str("lowerfirst"), + Word::MailboxExists => f.write_str("mailboxexists"), + Word::MailboxId => f.write_str("mailboxid"), + Word::MailboxIdExists => f.write_str("mailboxidexists"), + Word::Matches => f.write_str("matches"), + Word::Message => f.write_str("message"), + Word::Metadata => f.write_str("metadata"), + Word::MetadataExists => f.write_str("metadataexists"), + Word::Mime => f.write_str("mime"), + Word::Name => f.write_str("name"), + Word::Not => f.write_str("not"), + Word::Notify => f.write_str("notify"), + Word::NotifyMethodCapability => f.write_str("notify_method_capability"), + Word::Once => f.write_str("once"), + Word::Optional => f.write_str("optional"), + Word::Options => f.write_str("options"), + Word::OriginalZone => f.write_str("originalzone"), + Word::Over => f.write_str("over"), + Word::Param => f.write_str("param"), + Word::Percent => f.write_str("percent"), + Word::Personal => f.write_str("personal"), + Word::QuoteRegex => f.write_str("quoteregex"), + Word::QuoteWildcard => f.write_str("quotewildcard"), + Word::Raw => f.write_str("raw"), + Word::Redirect => f.write_str("redirect"), + Word::Regex => f.write_str("regex"), + Word::Reject => f.write_str("reject"), + Word::RemoveFlag => f.write_str("removeflag"), + Word::Replace => f.write_str("replace"), + Word::Require => f.write_str("require"), + Word::Ret => f.write_str("ret"), + Word::Return => f.write_str("return"), + Word::Seconds => f.write_str("seconds"), + Word::ServerMetadata => f.write_str("servermetadata"), + Word::ServerMetadataExists => f.write_str("servermetadataexists"), + Word::Set => f.write_str("set"), + Word::SetFlag => f.write_str("setflag"), + Word::Size => f.write_str("size"), + Word::SpamTest => f.write_str("spamtest"), + Word::SpecialUse => f.write_str("specialuse"), + Word::SpecialUseExists => f.write_str("specialuse_exists"), + Word::Stop => f.write_str("stop"), + Word::String => f.write_str("string"), + Word::Subject => f.write_str("subject"), + Word::Subtype => f.write_str("subtype"), + Word::Text => f.write_str("text"), + Word::True => f.write_str("true"), + Word::Type => f.write_str("type"), + Word::Under => f.write_str("under"), + Word::UniqueId => f.write_str("uniqueid"), + Word::Upper => f.write_str("upper"), + Word::UpperFirst => f.write_str("upperfirst"), + Word::User => f.write_str("user"), + Word::Vacation => f.write_str("vacation"), + Word::ValidExtList => f.write_str("valid_ext_list"), + Word::ValidNotifyMethod => f.write_str("valid_notify_method"), + Word::Value => f.write_str("value"), + Word::VirusTest => f.write_str("virustest"), + Word::Zone => f.write_str("zone"), + Word::Eval => f.write_str("eval"), + Word::Local => f.write_str("local"), + Word::While => f.write_str("while"), + Word::Let => f.write_str("let"), + Word::Continue => f.write_str("continue"), + } + } +} diff --git a/vendor/sieve-rs/src/compiler/mod.rs b/vendor/sieve-rs/src/compiler/mod.rs new file mode 100644 index 0000000..7212142 --- /dev/null +++ b/vendor/sieve-rs/src/compiler/mod.rs @@ -0,0 +1,662 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use self::{ + grammar::{AddressPart, Capability}, + lexer::tokenizer::TokenInfo, +}; +use crate::{Compiler, Envelope, FunctionMap, runtime::RuntimeError}; +use ahash::AHashMap; +use arc_swap::ArcSwap; +use mail_parser::HeaderName; +use std::{borrow::Cow, fmt::Display, sync::Arc}; + +pub mod grammar; +pub mod lexer; + +#[derive(Debug)] +pub struct CompileError { + line_num: usize, + line_pos: usize, + error_type: ErrorType, +} + +#[derive(Debug)] +pub enum ErrorType { + InvalidCharacter(u8), + InvalidNumber(String), + InvalidMatchVariable(usize), + InvalidUnicodeSequence(u32), + InvalidNamespace(String), + InvalidRegex(String), + InvalidExpression(String), + InvalidUtf8String, + InvalidHeaderName, + InvalidArguments, + InvalidAddress, + InvalidURI, + InvalidEnvelope(String), + UnterminatedString, + UnterminatedComment, + UnterminatedMultiline, + UnterminatedBlock, + ScriptTooLong, + StringTooLong, + VariableTooLong, + VariableIsLocal(String), + HeaderTooLong, + ExpectedConstantString, + UnexpectedToken { + expected: Cow<'static, str>, + found: String, + }, + UnexpectedEOF, + TooManyNestedBlocks, + TooManyNestedTests, + TooManyNestedForEveryParts, + TooManyIncludes, + LabelAlreadyDefined(String), + LabelUndefined(String), + BreakOutsideLoop, + ContinueOutsideLoop, + UnsupportedComparator(String), + DuplicatedParameter, + UndeclaredCapability(Capability), + MissingTag(Cow<'static, str>), +} + +impl Default for Compiler { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr( + feature = "rkyv", + rkyv(serialize_bounds( + __S: rkyv::ser::Writer + rkyv::ser::Allocator, + __S::Error: rkyv::rancor::Source, + )) +)] +#[cfg_attr( + feature = "rkyv", + rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source)) +)] +#[cfg_attr( + feature = "rkyv", + rkyv(bytecheck( + bounds( + __C: rkyv::validation::ArchiveContext, + ) + )) +)] +pub(crate) enum Value { + Text(Arc), + Number(Number), + Variable(VariableType), + Regex(Regex), + List(#[cfg_attr(feature = "rkyv", rkyv(omit_bounds))] Vec), +} + +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +pub struct Regex { + #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))] + #[cfg_attr(any(test, feature = "serde"), serde(skip, default))] + pub regex: LazyRegex, + pub expr: String, +} + +#[derive(Debug, Clone)] +pub struct LazyRegex(pub Arc>>); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum VariableType { + Local(usize), + Match(usize), + Global(String), + Environment(String), + Envelope(Envelope), + Header(HeaderVariable<'static>), + Part(MessagePart), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub struct Transform { + pub variable: Box, + pub functions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub struct HeaderVariable<'x> { + pub name: Vec>, + pub part: HeaderPart, + pub index_hdr: i32, + pub index_part: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum MessagePart { + TextBody(bool), + HtmlBody(bool), + Contents, + Raw, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum HeaderPart { + Text, + Date, + Id, + Address(AddressPart), + ContentType(ContentTypePart), + Received(ReceivedPart), + Raw, + RawName, + Exists, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum ContentTypePart { + Type, + Subtype, + Attribute(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum ReceivedPart { + From(ReceivedHostname), + FromIp, + FromIpRev, + By(ReceivedHostname), + For, + With, + TlsVersion, + TlsCipher, + Id, + Ident, + Via, + Date, + DateRaw, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum ReceivedHostname { + Name, + Ip, + Any, +} + +#[derive(Debug, Clone, Copy)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Number { + Integer(i64), + Float(f64), +} + +impl Number { + #[cfg(test)] + pub fn to_float(&self) -> f64 { + match self { + Number::Integer(i) => *i as f64, + Number::Float(fl) => *fl, + } + } +} + +impl From for usize { + fn from(value: Number) -> Self { + match value { + Number::Integer(i) => i as usize, + Number::Float(fl) => fl as usize, + } + } +} + +impl Display for Number { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Number::Integer(i) => i.fmt(f), + Number::Float(fl) => fl.fmt(f), + } + } +} + +impl Compiler { + pub const VERSION: u32 = 2; + + pub fn new() -> Self { + Compiler { + max_script_size: 1024 * 1024, + max_string_size: 4096, + max_variable_name_size: 32, + max_nested_blocks: 15, + max_nested_tests: 15, + max_nested_foreverypart: 3, + max_match_variables: 30, + max_local_variables: 128, + max_header_size: 1024, + max_includes: 6, + functions: AHashMap::new(), + no_capability_check: false, + } + } + + pub fn set_max_header_size(&mut self, size: usize) { + self.max_header_size = size; + } + + pub fn with_max_header_size(mut self, size: usize) -> Self { + self.max_header_size = size; + self + } + + pub fn set_max_includes(&mut self, size: usize) { + self.max_includes = size; + } + + pub fn with_max_includes(mut self, size: usize) -> Self { + self.max_includes = size; + self + } + + pub fn set_max_nested_blocks(&mut self, size: usize) { + self.max_nested_blocks = size; + } + + pub fn with_max_nested_blocks(mut self, size: usize) -> Self { + self.max_nested_blocks = size; + self + } + + pub fn set_max_nested_tests(&mut self, size: usize) { + self.max_nested_tests = size; + } + + pub fn with_max_nested_tests(mut self, size: usize) -> Self { + self.max_nested_tests = size; + self + } + + pub fn set_max_nested_foreverypart(&mut self, size: usize) { + self.max_nested_foreverypart = size; + } + + pub fn with_max_nested_foreverypart(mut self, size: usize) -> Self { + self.max_nested_foreverypart = size; + self + } + + pub fn set_max_script_size(&mut self, size: usize) { + self.max_script_size = size; + } + + pub fn with_max_script_size(mut self, size: usize) -> Self { + self.max_script_size = size; + self + } + + pub fn set_max_string_size(&mut self, size: usize) { + self.max_string_size = size; + } + + pub fn with_max_string_size(mut self, size: usize) -> Self { + self.max_string_size = size; + self + } + + pub fn set_max_variable_name_size(&mut self, size: usize) { + self.max_variable_name_size = size; + } + + pub fn with_max_variable_name_size(mut self, size: usize) -> Self { + self.max_variable_name_size = size; + self + } + + pub fn set_max_match_variables(&mut self, size: usize) { + self.max_match_variables = size; + } + + pub fn with_max_match_variables(mut self, size: usize) -> Self { + self.max_match_variables = size; + self + } + + pub fn set_max_local_variables(&mut self, size: usize) { + self.max_local_variables = size; + } + + pub fn with_max_local_variables(mut self, size: usize) -> Self { + self.max_local_variables = size; + self + } + + pub fn register_functions(mut self, fnc_map: &mut FunctionMap) -> Self { + self.functions = std::mem::take(&mut fnc_map.map); + self + } + + pub fn with_no_capability_check(mut self, value: bool) -> Self { + self.no_capability_check = value; + self + } + + pub fn set_no_capability_check(&mut self, value: bool) { + self.no_capability_check = value; + } +} + +impl CompileError { + pub fn line_num(&self) -> usize { + self.line_num + } + + pub fn line_pos(&self) -> usize { + self.line_pos + } + + pub fn error_type(&self) -> &ErrorType { + &self.error_type + } +} + +impl PartialEq for Regex { + fn eq(&self, other: &Self) -> bool { + self.expr == other.expr + } +} + +impl Eq for Regex {} + +impl TokenInfo { + pub fn expected(self, expected: impl Into>) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.line_pos, + error_type: ErrorType::UnexpectedToken { + expected: expected.into(), + found: self.token.to_string(), + }, + } + } + + pub fn missing_tag(self, tag: impl Into>) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.line_pos, + error_type: ErrorType::MissingTag(tag.into()), + } + } + + pub fn custom(self, error_type: ErrorType) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.line_pos, + error_type, + } + } +} + +impl Default for LazyRegex { + fn default() -> Self { + Self(Arc::new(ArcSwap::new(Arc::new(None)))) + } +} + +impl Regex { + pub fn new(expr: String, regex: fancy_regex::Regex) -> Self { + Self { + expr, + regex: LazyRegex(Arc::new(ArcSwap::new(Arc::new(Some(regex))))), + } + } +} + +impl Display for CompileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.error_type() { + ErrorType::InvalidCharacter(value) => { + write!(f, "Invalid character {:?}", char::from(*value)) + } + ErrorType::InvalidNumber(value) => write!(f, "Invalid number {value:?}"), + ErrorType::InvalidMatchVariable(value) => { + write!(f, "Match variable {value} out of range") + } + ErrorType::InvalidUnicodeSequence(value) => { + write!(f, "Invalid Unicode sequence {value:04x}") + } + ErrorType::InvalidNamespace(value) => write!(f, "Invalid namespace {value:?}"), + ErrorType::InvalidRegex(value) => write!(f, "Invalid regular expression {value:?}"), + ErrorType::InvalidExpression(value) => write!(f, "Invalid expression {value}"), + ErrorType::InvalidUtf8String => write!(f, "Invalid UTF-8 string"), + ErrorType::InvalidHeaderName => write!(f, "Invalid header name"), + ErrorType::InvalidArguments => write!(f, "Invalid Arguments"), + ErrorType::InvalidAddress => write!(f, "Invalid Address"), + ErrorType::InvalidURI => write!(f, "Invalid URI"), + ErrorType::InvalidEnvelope(value) => write!(f, "Invalid envelope {value:?}"), + ErrorType::UnterminatedString => write!(f, "Unterminated string"), + ErrorType::UnterminatedComment => write!(f, "Unterminated comment"), + ErrorType::UnterminatedMultiline => write!(f, "Unterminated multi-line string"), + ErrorType::UnterminatedBlock => write!(f, "Unterminated block"), + ErrorType::ScriptTooLong => write!(f, "Sieve script is too large"), + ErrorType::StringTooLong => write!(f, "String is too long"), + ErrorType::VariableTooLong => write!(f, "Variable name is too long"), + ErrorType::VariableIsLocal(value) => { + write!(f, "Variable {value:?} was already defined as local") + } + ErrorType::HeaderTooLong => write!(f, "Header value is too long"), + ErrorType::ExpectedConstantString => write!(f, "Expected a constant string"), + ErrorType::UnexpectedToken { expected, found } => { + write!(f, "Expected token {expected:?} but found {found:?}") + } + ErrorType::UnexpectedEOF => write!(f, "Unexpected end of file"), + ErrorType::TooManyNestedBlocks => write!(f, "Too many nested blocks"), + ErrorType::TooManyNestedTests => write!(f, "Too many nested tests"), + ErrorType::TooManyNestedForEveryParts => { + write!(f, "Too many nested foreverypart blocks") + } + ErrorType::TooManyIncludes => write!(f, "Too many includes"), + ErrorType::LabelAlreadyDefined(value) => write!(f, "Label {value:?} already defined"), + ErrorType::LabelUndefined(value) => write!(f, "Label {value:?} does not exist"), + ErrorType::BreakOutsideLoop => write!(f, "Break used outside of foreverypart loop"), + ErrorType::ContinueOutsideLoop => write!(f, "Continue used outside of while loop"), + ErrorType::UnsupportedComparator(value) => { + write!(f, "Comparator {value:?} is not supported") + } + ErrorType::DuplicatedParameter => write!(f, "Duplicated argument"), + ErrorType::UndeclaredCapability(value) => { + write!(f, "Undeclared capability '{value}'") + } + ErrorType::MissingTag(value) => write!(f, "Missing tag {value:?}"), + }?; + + write!( + f, + " at line {}, column {}.", + self.line_num(), + self.line_pos() + ) + } +} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RuntimeError::TooManyIncludes => write!(f, ""), + RuntimeError::InvalidInstruction(value) => write!( + f, + "Script executed invalid instruction {:?} at line {}, column {}.", + value.name(), + value.line_pos(), + value.line_num() + ), + RuntimeError::ScriptErrorMessage(value) => { + write!(f, "Script reported error {value:?}.") + } + RuntimeError::CapabilityNotAllowed(value) => { + write!(f, "Capability '{value}' has been disabled.") + } + RuntimeError::CapabilityNotSupported(value) => { + write!(f, "Capability '{value}' not supported.") + } + RuntimeError::CPULimitReached => write!( + f, + "Script exceeded the maximum number of instructions allowed to execute." + ), + } + } +} + +#[cfg(test)] +mod tests { + use std::{fs, path::PathBuf}; + + use crate::Compiler; + + #[test] + fn parse_rfc() { + let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + test_dir.push("tests"); + test_dir.push("rfcs"); + let mut tests_run = 0; + + let compiler = Compiler::new().with_max_nested_foreverypart(10); + + for file_name in fs::read_dir(&test_dir).unwrap() { + let mut file_name = file_name.unwrap().path(); + if file_name.extension().is_some_and(|e| e == "sieve") { + println!("Parsing {}", file_name.display()); + + /*if !file_name + .file_name() + .unwrap() + .to_str() + .unwrap() + .contains("plugins") + { + let test = "true"; + continue; + }*/ + + let script = fs::read(&file_name).unwrap(); + file_name.set_extension("json"); + let expected_result = fs::read(&file_name).unwrap(); + + tests_run += 1; + + let sieve = compiler.compile(&script).unwrap(); + let json_sieve = serde_json::to_string_pretty( + &sieve + .instructions + .into_iter() + .enumerate() + .collect::>(), + ) + .unwrap(); + + if json_sieve.as_bytes() != expected_result { + file_name.set_extension("failed"); + fs::write(&file_name, json_sieve.as_bytes()).unwrap(); + panic!("Test failed, parsed sieve saved to {}", file_name.display()); + } + } + } + + assert!( + tests_run > 0, + "Did not find any tests to run in folder {}.", + test_dir.display() + ); + } +} diff --git a/vendor/sieve-rs/src/lib.rs b/vendor/sieve-rs/src/lib.rs new file mode 100644 index 0000000..da509b5 --- /dev/null +++ b/vendor/sieve-rs/src/lib.rs @@ -0,0 +1,981 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. + */ + +#![doc = include_str!("../README.md")] + +use std::{borrow::Cow, sync::Arc, vec::IntoIter}; + +use ahash::{AHashMap, AHashSet}; +use compiler::grammar::{ + Capability, + actions::action_redirect::{ByTime, Notify, Ret}, + instruction::Instruction, +}; +use mail_parser::{HeaderName, Message}; +use runtime::{Variable, context::ScriptStack}; + +pub mod compiler; +pub mod runtime; + +pub(crate) const MAX_MATCH_VARIABLES: u32 = 63; +pub(crate) const MAX_LOCAL_VARIABLES: u32 = 256; + +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub struct Sieve { + instructions: Vec, + num_vars: u32, + num_match_vars: u32, +} + +#[derive(Clone)] +pub struct Compiler { + // Settings + pub(crate) max_script_size: usize, + pub(crate) max_string_size: usize, + pub(crate) max_variable_name_size: usize, + pub(crate) max_nested_blocks: usize, + pub(crate) max_nested_tests: usize, + pub(crate) max_nested_foreverypart: usize, + pub(crate) max_match_variables: usize, + pub(crate) max_local_variables: usize, + pub(crate) max_header_size: usize, + pub(crate) max_includes: usize, + pub(crate) no_capability_check: bool, + + // Functions + pub(crate) functions: AHashMap, +} + +pub type Function = for<'x> fn(&'x Context<'x>, Vec) -> Variable; + +#[derive(Default, Clone)] +pub struct FunctionMap { + pub(crate) map: AHashMap, + pub(crate) functions: Vec, +} + +#[derive(Debug, Clone)] +pub struct Runtime { + pub(crate) allowed_capabilities: AHashSet, + pub(crate) valid_notification_uris: AHashSet>, + pub(crate) valid_ext_lists: AHashSet>, + pub(crate) protected_headers: Vec>, + pub(crate) environment: AHashMap, Variable>, + pub(crate) metadata: Vec<(Metadata, Cow<'static, str>)>, + pub(crate) include_scripts: AHashMap>, + pub(crate) local_hostname: Cow<'static, str>, + pub(crate) functions: Vec, + + pub(crate) max_nested_includes: usize, + pub(crate) cpu_limit: usize, + pub(crate) max_variable_size: usize, + pub(crate) max_redirects: usize, + pub(crate) max_received_headers: usize, + pub(crate) max_header_size: usize, + pub(crate) max_out_messages: usize, + + pub(crate) default_vacation_expiry: u64, + pub(crate) default_duplicate_expiry: u64, + + pub(crate) vacation_use_orig_rcpt: bool, + pub(crate) vacation_default_subject: Cow<'static, str>, + pub(crate) vacation_subject_prefix: Cow<'static, str>, +} + +#[derive(Clone, Debug)] +pub struct Context<'x> { + #[cfg(test)] + pub(crate) runtime: Runtime, + #[cfg(not(test))] + pub(crate) runtime: &'x Runtime, + pub(crate) user_address: Cow<'x, str>, + pub(crate) user_full_name: Cow<'x, str>, + pub(crate) current_time: i64, + + pub(crate) message: Message<'x>, + pub(crate) message_size: usize, + pub(crate) envelope: Vec<(Envelope, Variable)>, + pub(crate) metadata: Vec<(Metadata, Cow<'x, str>)>, + + pub(crate) part: u32, + pub(crate) part_iter: IntoIter, + pub(crate) part_iter_stack: Vec<(u32, IntoIter)>, + + pub(crate) spam_status: SpamStatus, + pub(crate) virus_status: VirusStatus, + + pub(crate) pos: usize, + pub(crate) test_result: bool, + pub(crate) script_cache: AHashMap>, + pub(crate) script_stack: Vec, + pub(crate) vars_global: AHashMap, Variable>, + pub(crate) vars_env: AHashMap, Variable>, + pub(crate) vars_local: Vec, + pub(crate) vars_match: Vec, + pub(crate) expr_stack: Vec, + pub(crate) expr_pos: usize, + + pub(crate) queued_events: IntoIter, + pub(crate) final_event: Option, + pub(crate) last_message_id: usize, + pub(crate) main_message_id: usize, + + pub(crate) has_changes: bool, + pub(crate) num_redirects: usize, + pub(crate) num_instructions: usize, + pub(crate) num_out_messages: usize, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Script { + Personal(String), + Global(String), +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Envelope { + From, + To, + ByTimeAbsolute, + ByTimeRelative, + ByMode, + ByTrace, + Notify, + Orcpt, + Ret, + Envid, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub enum Metadata { + Server { annotation: T }, + Mailbox { name: T, annotation: T }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Event { + IncludeScript { + name: Script, + optional: bool, + }, + MailboxExists { + mailboxes: Vec, + special_use: Vec, + }, + ListContains { + lists: Vec, + values: Vec, + match_as: MatchAs, + }, + DuplicateId { + id: String, + expiry: u64, + last: bool, + }, + SetEnvelope { + envelope: Envelope, + value: String, + }, + Function { + id: ExternalId, + arguments: Vec, + }, + + // Actions + Keep { + flags: Vec, + message_id: usize, + }, + Discard, + Reject { + extended: bool, + reason: String, + }, + FileInto { + folder: String, + flags: Vec, + mailbox_id: Option, + special_use: Option, + create: bool, + message_id: usize, + }, + SendMessage { + recipient: Recipient, + notify: Notify, + return_of_content: Ret, + by_time: ByTime, + message_id: usize, + }, + Notify { + from: Option, + importance: Importance, + options: Vec, + message: String, + method: String, + }, + CreatedMessage { + message_id: usize, + message: Vec, + }, +} + +pub type ExternalId = u32; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive) +)] +pub(crate) struct FileCarbonCopy { + pub mailbox: T, + pub mailbox_id: Option, + pub create: bool, + pub flags: Vec, + pub special_use: Option, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Importance { + High, + Normal, + Low, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum MatchAs { + Octet, + Lowercase, + Number, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Recipient { + Address(String), + List(String), + Group(Vec), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Input { + True, + False, + FncResult(Variable), + Script { name: Script, script: Arc }, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Mailbox { + Name(String), + Id(String), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SpamStatus { + Unknown, + Ham, + MaybeSpam(f64), + Spam, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum VirusStatus { + Unknown, + Clean, + Replaced, + Cured, + MaybeVirus, + Virus, +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::{Path, PathBuf}, + }; + + use ahash::{AHashMap, AHashSet}; + use mail_parser::{ + Encoding, HeaderValue, Message, MessageParser, MessagePart, PartType, + parsers::MessageStream, + }; + + use crate::{ + Compiler, Context, Envelope, Event, FunctionMap, Input, Mailbox, Recipient, Runtime, + SpamStatus, VirusStatus, + compiler::grammar::Capability, + runtime::{Variable, actions::action_mime::reset_test_boundary}, + }; + + impl Variable { + pub fn unwrap_string(self) -> String { + self.to_string().into_owned() + } + } + + #[test] + fn test_suite() { + let mut tests = Vec::new(); + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests"); + + read_dir(path, &mut tests); + + for test in tests { + /*if !test + .file_name() + .unwrap() + .to_str() + .unwrap() + .contains("expressions") + { + continue; + }*/ + println!("===== {} =====", test.display()); + run_test(&test); + } + } + + fn read_dir(path: PathBuf, files: &mut Vec) { + for entry in fs::read_dir(path).unwrap() { + let entry = entry.unwrap().path(); + if entry.is_dir() { + read_dir(entry, files); + } else if entry + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .eq("svtest") + { + files.push(entry); + } + } + } + + fn run_test(script_path: &Path) { + let mut fnc_map = FunctionMap::new() + .with_function("trim", |_, v| match v.into_iter().next().unwrap() { + crate::runtime::Variable::String(s) => s.trim().to_string().into(), + v => v.to_string().into(), + }) + .with_function("len", |_, v| v[0].to_string().len().into()) + .with_function("count", |_, v| { + v[0].as_array().map_or(0, |arr| arr.len()).into() + }) + .with_function("to_lowercase", |_, v| { + v[0].to_string().to_lowercase().to_string().into() + }) + .with_function("to_uppercase", |_, v| { + v[0].to_string().to_uppercase().to_string().into() + }) + .with_function("is_uppercase", |_, v| { + v[0].to_string() + .as_ref() + .chars() + .filter(|c| c.is_alphabetic()) + .all(|c| c.is_uppercase()) + .into() + }) + .with_function("is_ascii", |_, v| { + v[0].to_string().as_ref().is_ascii().into() + }) + .with_function("char_count", |_, v| { + v[0].to_string().as_ref().chars().count().into() + }) + .with_function("lines", |_, v| { + v[0].to_string() + .lines() + .map(|line| Variable::from(line.to_string())) + .collect::>() + .into() + }) + .with_function_args( + "contains", + |_, v| v[0].to_string().contains(v[1].to_string().as_ref()).into(), + 2, + ) + .with_function_args( + "eq_lowercase", + |_, v| { + v[0].to_string() + .as_ref() + .eq_ignore_ascii_case(v[1].to_string().as_ref()) + .into() + }, + 2, + ) + .with_function_args( + "concat_three", + |_, v| format!("{}-{}-{}", v[0], v[1], v[2]).into(), + 3, + ) + .with_function_args( + "in_array", + |_, v| { + v[0].as_array() + .is_some_and(|arr| arr.contains(&v[1])) + .into() + }, + 2, + ) + .with_external_function("ext_zero", 0, 0) + .with_external_function("ext_one", 1, 1) + .with_external_function("ext_two", 2, 2) + .with_external_function("ext_three", 3, 3) + .with_external_function("ext_true", 4, 0) + .with_external_function("ext_false", 5, 0); + let mut compiler = Compiler::new() + .with_max_string_size(10240) + .register_functions(&mut fnc_map); + + let mut ancestors = script_path.ancestors(); + ancestors.next(); + let base_path = ancestors.next().unwrap(); + let script = compiler + .compile(&add_crlf(&fs::read(script_path).unwrap())) + .unwrap(); + + let mut input = Input::script("", script); + let mut current_test = String::new(); + let mut raw_message_: Option> = None; + let mut prev_state = None; + let mut mailboxes = Vec::new(); + let mut lists: AHashMap> = AHashMap::new(); + let mut duplicated_ids = AHashSet::new(); + let mut actions = Vec::new(); + + 'outer: loop { + let runtime = Runtime::new() + .with_protected_header("Auto-Submitted") + .with_protected_header("Received") + .with_valid_notification_uri("mailto") + .with_max_out_messages(100) + .with_capability(Capability::While) + .with_capability(Capability::Expressions) + .with_functions(&mut fnc_map.clone()); + let mut instance = Context::new( + &runtime, + Message { + parts: vec![MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text("".into()), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }], + raw_message: b""[..].into(), + ..Default::default() + }, + ); + let raw_message = raw_message_.take().unwrap_or_default(); + instance.message = + MessageParser::new() + .parse(&raw_message) + .unwrap_or_else(|| Message { + html_body: vec![], + text_body: vec![], + attachments: vec![], + parts: vec![MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text("".into()), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }], + raw_message: b""[..].into(), + }); + instance.message_size = raw_message.len(); + if let Some((pos, script_cache, script_stack, vars_global, vars_local, vars_match)) = + prev_state.take() + { + instance.pos = pos; + instance.script_cache = script_cache; + instance.script_stack = script_stack; + instance.vars_global = vars_global; + instance.vars_local = vars_local; + instance.vars_match = vars_match; + } + instance.set_env_variable("vnd.inbuxa.default_mailbox", "INBOX"); + instance.set_env_variable("vnd.inbuxa.username", "john.doe"); + instance.set_user_address("MAILER-DAEMON"); + if let Some(addr) = instance + .message + .from() + .and_then(|a| a.first()) + .and_then(|a| a.address.as_ref()) + { + instance.set_envelope(Envelope::From, addr.to_string()); + } + if let Some(addr) = instance + .message + .to() + .and_then(|a| a.first()) + .and_then(|a| a.address.as_ref()) + { + instance.set_envelope(Envelope::To, addr.to_string()); + } + + while let Some(event) = instance.run(input) { + match event.unwrap() { + Event::IncludeScript { name, optional } => { + let mut include_path = PathBuf::from(base_path); + include_path.push(if matches!(name, crate::Script::Personal(_)) { + "included" + } else { + "included-global" + }); + include_path.push(format!("{name}.sieve")); + + if let Ok(bytes) = fs::read(include_path.as_path()) { + let script = compiler.compile(&add_crlf(&bytes)).unwrap(); + input = Input::script(name, script); + } else if optional { + input = Input::False; + } else { + panic!("Script {} not found.", include_path.display()); + } + } + Event::MailboxExists { + mailboxes: mailboxes_, + special_use, + } => { + for action in &actions { + if let Event::FileInto { folder, create, .. } = action + && *create + && !mailboxes.contains(folder) + { + mailboxes.push(folder.to_string()); + } + } + input = (special_use.is_empty() + && mailboxes_.iter().all(|n| { + if let Mailbox::Name(n) = n { + mailboxes.contains(n) + } else { + false + } + })) + .into(); + } + Event::ListContains { + lists: lists_, + values, + .. + } => { + let mut result = false; + 'list: for list in &lists_ { + if let Some(list) = lists.get(list) { + for value in &values { + if list.contains(value) { + result = true; + break 'list; + } + } + } + } + + input = result.into(); + } + Event::DuplicateId { id, .. } => { + input = duplicated_ids.contains(&id).into(); + } + Event::Function { id, arguments } => { + if id == u32::MAX { + // Test functions + input = Input::True; + let mut arguments = arguments.into_iter(); + let command = arguments.next().unwrap().unwrap_string(); + let mut params = + arguments.map(|arg| arg.unwrap_string()).collect::>(); + + match command.as_str() { + "test" => { + current_test = params.pop().unwrap(); + println!("Running test '{current_test}'..."); + } + "test_set" => { + let mut params = params.into_iter(); + let target = params.next().expect("test_set parameter"); + if target == "message" { + let value = params.next().unwrap(); + raw_message_ = if value.eq_ignore_ascii_case(":smtp") { + let mut message = None; + for action in actions.iter().rev() { + if let Event::SendMessage { message_id, .. } = + action + { + let message_ = actions + .iter() + .find_map(|item| { + if let Event::CreatedMessage { + message_id: message_id_, + message, + } = item + && message_id == message_id_ + { + return Some(message); + } + None + }) + .unwrap(); + /*println!( + "<[{}]>", + std::str::from_utf8(message_).unwrap() + );*/ + message = message_.into(); + break; + } + } + message.expect("No SMTP message found").to_vec().into() + } else { + value.into_bytes().into() + }; + prev_state = ( + instance.pos, + instance.script_cache, + instance.script_stack, + instance.vars_global, + instance.vars_local, + instance.vars_match, + ) + .into(); + + continue 'outer; + } else if let Some(envelope) = target.strip_prefix("envelope.") + { + let envelope = + Envelope::try_from(envelope.to_string()).unwrap(); + instance.envelope.retain(|(e, _)| e != &envelope); + instance.set_envelope(envelope, params.next().unwrap()); + } else if target == "currentdate" { + let bytes = params.next().unwrap().into_bytes(); + if let HeaderValue::DateTime(dt) = + MessageStream::new(&bytes).parse_date() + { + instance.current_time = dt.to_timestamp(); + } else { + panic!("Invalid currentdate"); + } + } else { + panic!("test_set {target} not implemented."); + } + } + "test_message" => { + let mut params = params.into_iter(); + input = match params.next().unwrap().as_str() { + ":folder" => { + let folder_name = params.next().expect("test_message folder name"); + matches!(&instance.final_event, Some(Event::Keep { .. })) || + actions.iter().any(|a| if !folder_name.eq_ignore_ascii_case("INBOX") { + matches!(a, Event::FileInto { folder, .. } if folder == &folder_name ) + } else { + matches!(a, Event::Keep { .. }) + }) + } + ":smtp" => { + actions.iter().any(|a| matches!(a, Event::SendMessage { .. } )) + } + param => panic!("Invalid test_message param '{param}'" ), + }.into(); + } + "test_assert_message" => { + let expected_message = + params.first().expect("test_set parameter"); + let built_message = instance.build_message(); + if expected_message.as_bytes() != built_message { + //fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap(); + print!("<["); + print!("{}", String::from_utf8(built_message).unwrap()); + println!("]>"); + panic!("Message built incorrectly at '{current_test}'"); + } + } + "test_config_set" => { + let mut params = params.into_iter(); + let name = params.next().unwrap(); + let value = params.next().expect("test_config_set value"); + + match name.as_str() { + "sieve_editheader_protected" + | "sieve_editheader_forbid_add" + | "sieve_editheader_forbid_delete" => { + if !value.is_empty() { + for header_name in value.split(' ') { + instance.runtime.set_protected_header( + header_name.to_string(), + ); + } + } else { + instance.runtime.protected_headers.clear(); + } + } + "sieve_variables_max_variable_size" => { + instance + .runtime + .set_max_variable_size(value.parse().unwrap()); + } + "sieve_valid_ext_list" => { + instance.runtime.set_valid_ext_list(value); + } + "sieve_ext_list_item" => { + lists + .entry(value) + .or_default() + .insert(params.next().expect("list item value")); + } + "sieve_duplicated_id" => { + duplicated_ids.insert(value); + } + "sieve_user_email" => { + instance.set_user_address(value); + } + "sieve_vacation_use_original_recipient" => { + instance.runtime.set_vacation_use_orig_rcpt( + value.eq_ignore_ascii_case("yes"), + ); + } + "sieve_vacation_default_subject" => { + instance.runtime.set_vacation_default_subject(value); + } + "sieve_vacation_default_subject_template" => { + instance.runtime.set_vacation_subject_prefix(value); + } + "sieve_spam_status" => { + instance.set_spam_status(SpamStatus::from_number( + value.parse().unwrap(), + )); + } + "sieve_spam_status_plus" => { + instance.set_spam_status( + match value.parse::().unwrap() { + 0 => SpamStatus::Unknown, + 100.. => SpamStatus::Spam, + n => SpamStatus::MaybeSpam((n as f64) / 100.0), + }, + ); + } + "sieve_virus_status" => { + instance.set_virus_status(VirusStatus::from_number( + value.parse().unwrap(), + )); + } + "sieve_editheader_max_header_size" => { + let mhs = if !value.is_empty() { + value.parse::().unwrap() + } else { + 1024 + }; + instance.runtime.set_max_header_size(mhs); + compiler.set_max_header_size(mhs); + } + "sieve_include_max_includes" => { + compiler.set_max_includes(if !value.is_empty() { + value.parse::().unwrap() + } else { + 3 + }); + } + "sieve_include_max_nesting_depth" => { + compiler.set_max_nested_blocks(if !value.is_empty() { + value.parse::().unwrap() + } else { + 3 + }); + } + param => panic!("Invalid test_config_set param '{param}'"), + } + } + "test_result_execute" => { + input = + (matches!(&instance.final_event, Some(Event::Keep { .. })) + || actions.iter().any(|a| { + matches!( + a, + Event::Keep { .. } + | Event::FileInto { .. } + | Event::SendMessage { .. } + ) + })) + .into(); + } + "test_result_action" => { + let param = + params.first().expect("test_result_action parameter"); + input = if param == "reject" { + (actions.iter().any(|a| matches!(a, Event::Reject { .. }))) + .into() + } else if param == "redirect" { + let param = params + .last() + .expect("test_result_action redirect address"); + (actions + .iter() + .any(|a| matches!(a, Event::SendMessage { recipient: Recipient::Address(address), .. } if address == param))) + .into() + } else if param == "keep" { + (matches!(&instance.final_event, Some(Event::Keep { .. })) + || actions + .iter() + .any(|a| matches!(a, Event::Keep { .. }))) + .into() + } else if param == "send_message" { + (actions + .iter() + .any(|a| matches!(a, Event::SendMessage { .. }))) + .into() + } else { + panic!("test_result_action {param} not implemented"); + }; + } + "test_result_action_count" => { + input = (actions.len() + == params.first().unwrap().parse::().unwrap()) + .into(); + } + "test_imap_metadata_set" => { + let mut params = params.into_iter(); + let first = params.next().expect("metadata parameter"); + let (mailbox, annotation) = if first == ":mailbox" { + ( + params.next().expect("metadata mailbox name").into(), + params.next().expect("metadata annotation name"), + ) + } else { + (None, first) + }; + let value = params.next().expect("metadata value"); + if let Some(mailbox) = mailbox { + instance.set_medatata((mailbox, annotation), value); + } else { + instance.set_medatata(annotation, value); + } + } + "test_mailbox_create" => { + mailboxes.push(params.pop().expect("mailbox to create")); + } + "test_result_reset" => { + actions.clear(); + instance.final_event = Event::Keep { + flags: vec![], + message_id: 0, + } + .into(); + instance.metadata.clear(); + instance.has_changes = false; + instance.num_redirects = 0; + instance.runtime.vacation_use_orig_rcpt = false; + mailboxes.clear(); + lists.clear(); + reset_test_boundary(); + } + "test_script_compile" => { + let mut include_path = PathBuf::from(base_path); + include_path.push(params.first().unwrap()); + + if let Ok(bytes) = fs::read(include_path.as_path()) { + let result = compiler.compile(&add_crlf(&bytes)); + /*if let Err(err) = &result { + println!("Error: {:?}", err); + }*/ + input = result.is_ok().into(); + } else { + panic!("Script {} not found.", include_path.display()); + } + } + "test_config_reload" => (), + "test_fail" => { + panic!( + "Test '{}' failed: {}", + current_test, + params.pop().unwrap() + ); + } + _ => panic!("Test command {command} not implemented."), + } + } else { + let result = match id { + 0 => Variable::from("my_value"), + 1 => Variable::from(arguments[0].to_string().to_uppercase()), + 2 => Variable::from(format!( + "{}-{}", + arguments[0].to_string(), + arguments[1].to_string() + )), + 3 => Variable::from(format!( + "{}-{}-{}", + arguments[0].to_string(), + arguments[1].to_string(), + arguments[2].to_string() + )), + 4 => true.into(), + 5 => false.into(), + _ => { + panic!("Unknown external function {id}"); + } + }; + + input = result.into(); + } + } + + action => { + actions.push(action); + input = true.into(); + } + } + } + + return; + } + } + + fn add_crlf(bytes: &[u8]) -> Vec { + let mut result = Vec::with_capacity(bytes.len()); + let mut last_ch = 0; + for &ch in bytes { + if ch == b'\n' && last_ch != b'\r' { + result.push(b'\r'); + } + result.push(ch); + last_ch = ch; + } + result + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_convert.rs b/vendor/sieve-rs/src/runtime/actions/action_convert.rs new file mode 100644 index 0000000..923a79c --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_convert.rs @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::{ + Encoding, Header, HeaderName, HeaderValue, MimeHeaders, PartType, + decoders::html::{html_to_text, text_to_html}, +}; + +use crate::{ + Context, compiler::grammar::actions::action_convert::Convert, runtime::tests::TestResult, +}; + +#[derive(Clone, Copy)] +enum Conversion { + TextToHtml, + TextPlainToHtml, + HtmlToText, +} + +impl Convert { + pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult { + let _from_media_type = ctx.eval_value(&self.from_media_type); + let _to_media_type = ctx.eval_value(&self.to_media_type); + + let from_media_type = _from_media_type.to_string(); + let to_media_type = _to_media_type.to_string(); + + if from_media_type.eq_ignore_ascii_case(to_media_type.as_ref()) { + return TestResult::Bool(false ^ self.is_not); + } + + let conversion = if (from_media_type.eq_ignore_ascii_case("text") + || from_media_type.starts_with("text/")) + && to_media_type.eq_ignore_ascii_case("text/html") + { + if from_media_type.eq_ignore_ascii_case("text") { + Conversion::TextPlainToHtml + } else { + Conversion::TextToHtml + } + } else if from_media_type.eq_ignore_ascii_case("text/html") + && to_media_type.eq_ignore_ascii_case("text/plain") + { + Conversion::HtmlToText + } else { + return TestResult::Bool(false ^ self.is_not); + }; + let mut did_convert = false; + for part in ctx.message.parts.iter_mut() { + let (new_body, ct) = match (&part.body, conversion) { + (PartType::Html(html), Conversion::HtmlToText) => ( + PartType::Text(html_to_text(html.as_ref()).into()), + "text/plain; charset=utf8", + ), + (PartType::Text(text), Conversion::TextToHtml) => ( + PartType::Html(text_to_html(text.as_ref()).into()), + "text/html; charset=utf8", + ), + (PartType::Text(text), Conversion::TextPlainToHtml) + if part + .content_type() + .and_then(|ct| ct.c_subtype.as_ref()) + .is_some_and(|st| st.eq_ignore_ascii_case("plain")) => + { + ( + PartType::Html(text_to_html(text.as_ref()).into()), + "text/html; charset=utf8", + ) + } + _ => { + continue; + } + }; + part.headers = vec![Header { + name: HeaderName::Other("Content-Type".into()), + value: HeaderValue::Text(ct.to_string().into()), + offset_start: 0, + offset_end: 0, + offset_field: 0, + }]; + ctx.message_size = ctx.message_size + ct.len() + new_body.len() + 16 + - (if part.offset_body != 0 { + (part.offset_end - part.offset_header) as usize + } else { + part.body.len() + }); + part.offset_body = 0; + part.body = new_body; + part.encoding = Encoding::QuotedPrintable; //Used as non-mime flag + did_convert = true; + } + + if did_convert { + ctx.has_changes = true; + } + + TestResult::Bool(did_convert ^ self.is_not) + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_editheader.rs b/vendor/sieve-rs/src/runtime/actions/action_editheader.rs new file mode 100644 index 0000000..6c4c88b --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_editheader.rs @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::borrow::Cow; + +use mail_parser::{Header, HeaderName, HeaderValue}; + +use crate::{ + Context, + compiler::grammar::{ + MatchType, + actions::{ + action_editheader::{AddHeader, DeleteHeader}, + action_mime::MimeOpts, + }, + }, +}; + +impl AddHeader { + pub(crate) fn exec(&self, ctx: &mut Context) { + let header_name__ = ctx.eval_value(&self.field_name); + let header_name_ = header_name__.to_string(); + let mut header_name = String::with_capacity(header_name_.len()); + + for ch in header_name_.chars() { + if ch.is_alphanumeric() || ch == '-' { + header_name.push(ch); + } + } + + if !header_name.is_empty() + && let Some(header_name) = HeaderName::parse(header_name) + && !ctx.runtime.protected_headers.contains(&header_name) + { + ctx.has_changes = true; + ctx.insert_header( + ctx.part, + header_name, + ctx.eval_value(&self.value) + .to_string() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size), + self.last, + ) + } + } +} + +impl DeleteHeader { + pub(crate) fn exec(&self, ctx: &mut Context) { + let header_name__ = ctx.eval_value(&self.field_name); + let header_name_ = header_name__.to_string(); + let header_name = if let Some(header_name) = HeaderName::parse(header_name_.as_ref()) { + header_name + } else { + return; + }; + let value_patterns = ctx.eval_values(&self.value_patterns); + let mut deleted_headers = Vec::new(); + let mut deleted_bytes = 0; + + if ctx.runtime.protected_headers.contains(&header_name) { + return; + } + + ctx.find_headers( + &[header_name], + self.index, + self.mime_anychild, + |header, part_id, header_pos| { + if !value_patterns.is_empty() { + let did_match = ctx.find_header_values(header, &MimeOpts::None, |value| { + for (pattern_expr, pattern) in + value_patterns.iter().zip(self.value_patterns.iter()) + { + if match &self.match_type { + MatchType::Is => self.comparator.is(&value, pattern_expr), + MatchType::Contains => self + .comparator + .contains(value, pattern_expr.to_string().as_ref()), + MatchType::Value(rel_match) => { + self.comparator.relational(rel_match, &value, pattern_expr) + } + MatchType::Matches(_) => self.comparator.matches( + value, + pattern_expr.to_string().as_ref(), + 0, + &mut Vec::new(), + ), + MatchType::Regex(_) => self.comparator.regex( + pattern, + pattern_expr, + value, + 0, + &mut Vec::new(), + ), + MatchType::Count(_) => false, + MatchType::List => false, + } { + return true; + } + } + false + }); + + if !did_match { + return false; + } + } + + if header.offset_end != 0 { + deleted_bytes += (header.offset_end - header.offset_field) as usize; + } else { + deleted_bytes += header.name.as_str().len() + header.value.len() + 4; + } + deleted_headers.push((part_id, header_pos)); + + false + }, + ); + + if !deleted_headers.is_empty() { + ctx.has_changes = true; + for (part_id, header_pos) in deleted_headers.iter().rev() { + ctx.message.parts[*part_id as usize] + .headers + .remove(*header_pos); + } + } + + ctx.message_size -= deleted_bytes; + } +} + +pub(crate) trait RemoveCrLf { + fn remove_crlf(&self, max_len: usize) -> String; +} + +impl RemoveCrLf for &str { + fn remove_crlf(&self, max_len: usize) -> String { + let mut header_value = String::with_capacity(self.len()); + for ch in self.chars() { + if !['\n', '\r'].contains(&ch) { + if header_value.len() + ch.len_utf8() <= max_len { + header_value.push(ch); + } else { + return header_value; + } + } + } + header_value + } +} + +impl<'x> Context<'x> { + pub(crate) fn insert_header( + &mut self, + part_id: u32, + header_name: HeaderName<'x>, + header_value: impl Into>, + last: bool, + ) { + let header_value = header_value.into(); + self.message_size += header_name.len() + header_value.len() + 4; + let header = Header { + name: header_name, + value: HeaderValue::Text(header_value), + offset_start: 0, + offset_end: 0, + offset_field: 0, + }; + + if !last { + self.message.parts[part_id as usize] + .headers + .insert(0, header); + } else { + self.message.parts[part_id as usize].headers.push(header); + } + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_fileinto.rs b/vendor/sieve-rs/src/runtime/actions/action_fileinto.rs new file mode 100644 index 0000000..6b0c727 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_fileinto.rs @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{Context, Event, compiler::grammar::actions::action_fileinto::FileInto}; + +impl FileInto { + pub(crate) fn exec(&self, ctx: &mut Context) { + let folder = ctx.eval_value(&self.folder).to_string().into_owned(); + let mut events = Vec::with_capacity(2); + if let Some(event) = ctx.build_message_id() { + events.push(event); + } + + if !self.copy + && !matches!(&ctx.final_event, Some(Event::Keep { flags, .. }) if !flags.is_empty()) + { + ctx.final_event = None; + } + + events.push(Event::FileInto { + folder, + flags: ctx.get_local_or_global_flags(&self.flags), + mailbox_id: self + .mailbox_id + .as_ref() + .map(|mi| ctx.eval_value(mi).to_string().into_owned()), + special_use: self + .special_use + .as_ref() + .map(|su| ctx.eval_value(su).to_string().into_owned()), + create: self.create, + message_id: ctx.main_message_id, + }); + + ctx.queued_events = events.into_iter(); + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_flags.rs b/vendor/sieve-rs/src/runtime/actions/action_flags.rs new file mode 100644 index 0000000..fe8d253 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_flags.rs @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + Context, + compiler::{ + Value, VariableType, + grammar::actions::action_flags::{Action, EditFlags}, + }, +}; + +impl EditFlags { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut var_name_ = None; + let var_name = self.name.as_ref().unwrap_or_else(|| { + var_name_.get_or_insert_with(|| VariableType::Global("__flags".to_string())) + }); + + match &self.action { + Action::Set => { + let mut flags_lc = Vec::new(); + let mut flags = String::new(); + ctx.tokenize_flags(&self.flags, |flag| { + let flag_lc = flag.to_lowercase(); + if !flags_lc.contains(&flag_lc) { + if !flags.is_empty() { + flags.push(' '); + } + flags.push_str(flag); + flags_lc.push(flag_lc); + } + false + }); + ctx.set_variable(var_name, flags.into()); + } + Action::Add => { + let mut new_flags = ctx + .get_variable(var_name) + .map(|v| v.to_string()) + .unwrap_or_default() + .into_owned(); + let mut current_flags = new_flags + .split(' ') + .map(|f| f.to_lowercase()) + .collect::>(); + + ctx.tokenize_flags(&self.flags, |flag| { + let flag_lc = flag.to_lowercase(); + if !current_flags.contains(&flag_lc) { + if !new_flags.is_empty() { + new_flags.push(' '); + } + new_flags.push_str(flag); + current_flags.push(flag_lc); + } + false + }); + ctx.set_variable(var_name, new_flags.into()); + } + Action::Remove => { + let mut current_flags = Vec::new(); + let mut current_flags_lc = Vec::new(); + let flags = ctx + .get_variable(var_name) + .map(|v| v.to_string().into_owned()) + .unwrap_or_default(); + + for flag in flags.split(' ') { + current_flags.push(flag); + current_flags_lc.push(flag.to_lowercase()); + } + ctx.tokenize_flags(&self.flags, |flag| { + let flag = flag.to_lowercase(); + if let Some(pos) = current_flags_lc.iter().position(|lflag| lflag == &flag) { + current_flags.swap_remove(pos); + current_flags_lc.swap_remove(pos); + } + false + }); + ctx.set_variable(var_name, current_flags.join(" ").into()); + } + } + } +} + +impl Context<'_> { + pub(crate) fn tokenize_flags( + &self, + strings: &[Value], + mut cb: impl FnMut(&str) -> bool, + ) -> bool { + for (pos, string) in strings.iter().enumerate() { + let flag_ = self.eval_value(string); + let flag = flag_.to_string(); + if !flag.is_empty() { + if pos == 0 && strings.len() == 1 { + for flag in flag.split_ascii_whitespace() { + if !flag.is_empty() && cb(flag) { + return true; + } + } + } else if cb(flag.trim()) { + return true; + } + } + } + false + } + + pub(crate) fn get_local_flags(&self, strings: &[Value]) -> Vec { + let mut flags = Vec::new(); + self.tokenize_flags(strings, |flag| { + flags.push(flag.to_string()); + false + }); + flags + } + + pub(crate) fn get_global_flags(&self) -> Vec { + match self.vars_global.get("__flags") { + Some(flags) if !flags.is_empty() => flags + .to_string() + .split(' ') + .map(|s| s.to_string()) + .collect::>(), + _ => Vec::new(), + } + } + + pub(crate) fn get_local_or_global_flags(&self, strings: &[Value]) -> Vec { + if strings.is_empty() { + self.get_global_flags() + } else { + self.get_local_flags(strings) + } + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_include.rs b/vendor/sieve-rs/src/runtime/actions/action_include.rs new file mode 100644 index 0000000..e84ba09 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_include.rs @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::sync::Arc; + +use crate::{ + Context, Event, Script, Sieve, + compiler::grammar::actions::action_include::{Include, Location}, + runtime::RuntimeError, +}; + +pub(crate) enum IncludeResult { + Cached(Arc), + Event(Event), + Error(RuntimeError), + None, +} + +impl Include { + pub(crate) fn exec(&self, ctx: &Context) -> IncludeResult { + let script_name = ctx.eval_value(&self.value); + if !script_name.is_empty() { + let script_name = if self.location == Location::Global { + Script::Global(script_name.to_string().into_owned()) + } else { + Script::Personal(script_name.to_string().into_owned()) + }; + + let cached_script = ctx.script_cache.get(&script_name); + if !self.once || cached_script.is_none() { + if ctx.script_stack.len() < ctx.runtime.max_nested_includes { + if let Some(script) = cached_script + .or_else(|| ctx.runtime.include_scripts.get(script_name.as_str())) + { + return IncludeResult::Cached(script.clone()); + } else { + return IncludeResult::Event(Event::IncludeScript { + name: script_name, + optional: self.optional, + }); + } + } else { + return IncludeResult::Error(RuntimeError::TooManyIncludes); + } + } + } + + IncludeResult::None + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_mime.rs b/vendor/sieve-rs/src/runtime/actions/action_mime.rs new file mode 100644 index 0000000..eaadd29 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_mime.rs @@ -0,0 +1,582 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::cmp::Reverse; + +use mail_parser::{ + Encoding, HeaderName, Message, MessagePart, PartType, decoders::html::html_to_text, +}; + +use crate::{ + Context, Event, + compiler::{ + VariableType, + grammar::actions::action_mime::{Enclose, ExtractText, Replace}, + }, +}; + +use super::action_editheader::RemoveCrLf; + +#[cfg(not(test))] +use mail_builder::headers::message_id::generate_message_id_header; + +impl Replace { + pub(crate) fn exec(&self, ctx: &mut Context) { + // Delete children parts + let mut part_ids = ctx.find_nested_parts_ids(false); + part_ids.sort_unstable_by_key(|a| Reverse(*a)); + for part_id in part_ids { + ctx.message.parts.remove(part_id as usize); + } + ctx.has_changes = true; + + // Update part + let body = ctx.eval_value(&self.replacement).to_string().into_owned(); + let body_len = body.len(); + + let part = &mut ctx.message.parts[ctx.part as usize]; + + ctx.message_size = ctx.message_size + body_len + - (if part.offset_body != 0 { + (part.offset_end - part.offset_header) as usize + } else { + part.body.len() + }); + part.body = PartType::Text(body.into()); + part.encoding = if !self.mime { + Encoding::QuotedPrintable + } else { + Encoding::None + }; + part.offset_body = 0; + let prev_headers = std::mem::take(&mut part.headers); + let mut add_date = true; + let mut has_original_from = false; + + if ctx.part == 0 { + for mut header in prev_headers { + let mut size = (header.offset_end - header.offset_field) as usize; + match &header.name { + HeaderName::Subject => { + if self.subject.is_some() { + header.name = HeaderName::Other("Original-Subject".into()); + header.offset_field = header.offset_start; + size += "Original-".len(); + } + } + HeaderName::From => { + if self.from.is_some() { + header.name = HeaderName::Other("Original-From".into()); + header.offset_field = header.offset_start; + size += "Original-".len(); + } else { + has_original_from = true; + } + } + + HeaderName::To | HeaderName::Cc | HeaderName::Bcc | HeaderName::Received => (), + HeaderName::Date => { + add_date = false; + } + _ => continue, + } + ctx.message_size += size; + part.headers.push(header); + } + + // Add From + let mut add_from = true; + if let Some(from) = self.from.as_ref().map(|f| ctx.eval_value(f)) + && !from.is_empty() + { + ctx.insert_header( + 0, + HeaderName::Other("From".into()), + from.to_string() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size), + true, + ); + add_from = false; + } + if add_from && !has_original_from { + ctx.insert_header( + 0, + HeaderName::Other("From".to_string().into()), + ctx.user_from_field(), + true, + ); + } + + // Add Subject + if let Some(subject) = self.subject.as_ref().map(|f| ctx.eval_value(f)) + && !subject.is_empty() + { + ctx.insert_header( + 0, + HeaderName::Other("Subject".into()), + subject + .to_string() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size), + true, + ); + } + + // Add Date + if add_date { + #[cfg(not(test))] + let header_value = mail_builder::headers::date::Date::now().to_rfc822(); + #[cfg(test)] + let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string(); + + ctx.insert_header( + 0, + HeaderName::Other("Date".to_string().into()), + header_value, + true, + ); + } + + // Add Message-ID + let mut header_value = Vec::with_capacity(20); + #[cfg(not(test))] + generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap(); + #[cfg(test)] + header_value.extend_from_slice(b""); + + ctx.insert_header( + 0, + HeaderName::Other("Message-ID".to_string().into()), + String::from_utf8(header_value).unwrap(), + true, + ); + } + + if !self.mime { + ctx.insert_header( + ctx.part, + HeaderName::Other("Content-Type".into()), + "text/plain; charset=utf-8".to_string(), + true, + ); + } + } +} + +impl Enclose { + pub(crate) fn exec(&self, ctx: &mut Context) { + let body = ctx.eval_value(&self.value).to_string().into_owned(); + let subject = self + .subject + .as_ref() + .map(|s| { + ctx.eval_value(s) + .to_string() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size) + }) + .or_else(|| ctx.message.subject().map(|s| s.to_string())) + .unwrap_or_default(); + + let message = std::mem::take(&mut ctx.message); + #[cfg(test)] + let boundary = make_test_boundary(); + #[cfg(not(test))] + let boundary = mail_builder::mime::make_boundary("."); + + ctx.message_size += ((boundary.len() + 6) * 3) + body.len() + 2; + ctx.part = 0; + ctx.has_changes = true; + ctx.message = Message { + html_body: Vec::with_capacity(0), + text_body: Vec::with_capacity(0), + attachments: Vec::with_capacity(0), + parts: vec![ + MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Multipart(vec![1, 2]), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }, + MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text(body.into()), + encoding: Encoding::QuotedPrintable, // Flag non-mime part + offset_header: 0, + offset_body: 0, + offset_end: 0, + }, + MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Message(message), + encoding: Encoding::QuotedPrintable, // Flag non-mime part + offset_header: 0, + offset_body: 0, + offset_end: 0, + }, + ], + raw_message: b""[..].into(), + }; + + ctx.insert_header( + 0, + HeaderName::Other("Content-Type".into()), + format!("multipart/mixed; boundary=\"{boundary}\""), + true, + ); + ctx.insert_header(0, HeaderName::Other("Subject".into()), subject, true); + ctx.insert_header( + 1, + HeaderName::Other("Content-Type".into()), + "text/plain; charset=utf-8", + true, + ); + ctx.insert_header( + 2, + HeaderName::Other("Content-Type".into()), + "message/rfc822", + true, + ); + + let mut add_date = true; + let mut add_message_id = true; + let mut add_from = true; + + for header in &self.headers { + let header = ctx.eval_value(header); + if let Some((mut header_name, mut header_value)) = + header.to_string().as_ref().split_once(':') + { + header_name = header_name.trim(); + header_value = header_value.trim(); + if !header_value.is_empty() + && let Some(name) = HeaderName::parse(header_name) + && !ctx.runtime.protected_headers.contains(&name) + { + match &name { + HeaderName::Date => { + add_date = false; + } + HeaderName::From => { + add_from = false; + } + HeaderName::MessageId => { + add_message_id = false; + } + _ => (), + } + + ctx.insert_header( + 0, + HeaderName::Other(header_name.to_string().into()), + header_value.remove_crlf(ctx.runtime.max_header_size), + true, + ); + } + } + } + + if add_from { + ctx.insert_header( + 0, + HeaderName::Other("From".to_string().into()), + ctx.user_from_field(), + true, + ); + } + + if add_date { + #[cfg(not(test))] + let header_value = mail_builder::headers::date::Date::now().to_rfc822(); + #[cfg(test)] + let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string(); + + ctx.insert_header( + 0, + HeaderName::Other("Date".to_string().into()), + header_value, + true, + ); + } + + if add_message_id { + let mut header_value = Vec::with_capacity(20); + #[cfg(not(test))] + generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap(); + #[cfg(test)] + header_value.extend_from_slice(b""); + + ctx.insert_header( + 0, + HeaderName::Other("Message-ID".to_string().into()), + String::from_utf8(header_value).unwrap(), + true, + ); + } + } +} + +impl ExtractText { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut value = String::new(); + + if !ctx.part_iter_stack.is_empty() { + match ctx.message.parts.get(ctx.part as usize).map(|p| &p.body) { + Some(PartType::Text(text)) => { + value = if let Some(first) = &self.first { + text.chars().take(*first).collect() + } else { + text.as_ref().to_string() + }; + } + Some(PartType::Html(html)) => { + value = if let Some(first) = &self.first { + html_to_text(html.as_ref()).chars().take(*first).collect() + } else { + html_to_text(html.as_ref()) + }; + } + _ => (), + } + + if !self.modifiers.is_empty() && !value.is_empty() { + for modifier in &self.modifiers { + value = modifier.apply(&value, ctx); + } + } + } + + match &self.name { + VariableType::Local(var_id) => { + if let Some(var) = ctx.vars_local.get_mut(*var_id) { + *var = value.into(); + } else { + debug_assert!(false, "Non-existent local variable {var_id}"); + } + } + VariableType::Global(var_name) => { + ctx.vars_global + .insert(var_name.to_string().into(), value.into()); + } + VariableType::Envelope(env) => { + ctx.add_set_envelope_event(*env, value); + } + _ => (), + } + } +} + +enum StackItem<'x> { + Message(&'x Message<'x>), + Boundary(&'x str), + None, +} + +impl Context<'_> { + pub(crate) fn build_message_id(&mut self) -> Option { + if self.has_changes { + self.last_message_id += 1; + self.main_message_id = self.last_message_id; + self.has_changes = false; + let message = self.build_message(); + Some(Event::CreatedMessage { + message_id: self.main_message_id, + message, + }) + } else { + None + } + } + + pub(crate) fn build_message(&mut self) -> Vec { + let mut current_message = &self.message; + let mut current_boundary = ""; + let mut message = Vec::with_capacity(self.message_size); + let mut iter = [0u32].iter(); + let mut iter_stack = Vec::new(); + let mut last_offset = 0; + + 'outer: loop { + while let Some(part) = iter + .next() + .and_then(|p| current_message.parts.get(*p as usize)) + { + if last_offset > 0 { + message.extend_from_slice( + ¤t_message.raw_message + [last_offset as usize..part.offset_header as usize], + ); + } else if !current_boundary.is_empty() + && part.offset_end == 0 + && !matches!(iter_stack.last(), Some((StackItem::Message(_), _, _))) + { + message.extend_from_slice(b"\r\n--"); + message.extend_from_slice(current_boundary.as_bytes()); + message.extend_from_slice(b"\r\n"); + } + + let mut ct_pos = usize::MAX; + + for (header_pos, header) in part.headers.iter().enumerate() { + if header.offset_end != 0 { + if header.offset_field != header.offset_start { + message.extend_from_slice( + ¤t_message.raw_message + [header.offset_field as usize..header.offset_end as usize], + ); + } else { + // Renamed header + message.extend_from_slice(header.name.as_str().as_bytes()); + message.extend_from_slice(b":"); + message.extend_from_slice( + ¤t_message.raw_message + [header.offset_start as usize..header.offset_end as usize], + ); + } + } else { + if header.name == HeaderName::Other("Content-Type".into()) { + ct_pos = header_pos; + } + + message.extend_from_slice(header.name.as_str().as_bytes()); + message.extend_from_slice(b": "); + message.extend_from_slice(header.value.as_text().unwrap_or("").as_bytes()); + message.extend_from_slice(b"\r\n"); + } + } + + if part.offset_body != 0 || part.encoding != Encoding::None { + // Add CRLF unless this is a :mime replaced part + message.extend_from_slice(b"\r\n"); + } + + if part.offset_body != 0 { + // Original message part + + if let PartType::Multipart(subparts) = &part.body { + // Multiparts contain offsets of the entire part, do not add. + iter_stack.push(( + StackItem::None, + part, + std::mem::replace(&mut iter, subparts.iter()), + )); + last_offset = part.offset_body; + continue 'outer; + } else { + message.extend_from_slice( + ¤t_message.raw_message + [part.offset_body as usize..part.offset_end as usize], + ) + } + } else { + match &part.body { + PartType::Message(nested_message) => { + // Enclosed message + iter_stack.push(( + StackItem::Message(current_message), + part, + std::mem::replace(&mut iter, [0].iter()), + )); + current_message = nested_message; + continue 'outer; + } + PartType::Multipart(subparts) => { + // Multipart enclosing nested message, obtain MIME boundary + let prev_boundary = std::mem::replace( + &mut current_boundary, + if ct_pos != usize::MAX { + part.headers[ct_pos] + .value + .as_text() + .and_then(|h| h.split_once("boundary=\"")) + .and_then(|(_, h)| h.split_once('\"')) + .map(|(h, _)| h) + } else { + None + } + .unwrap_or("invalid-boundary"), + ); + + // Enclose multipart + iter_stack.push(( + StackItem::Boundary(prev_boundary), + part, + std::mem::replace(&mut iter, subparts.iter()), + )); + continue 'outer; + } + _ => { + // Replaced part + message.extend_from_slice(part.contents()); + } + } + } + last_offset = part.offset_end; + } + + if let Some((prev_item, prev_part, prev_iter)) = iter_stack.pop() { + match prev_item { + StackItem::Message(prev_message) => { + if last_offset > 0 { + if let Some(bytes) = + current_message.raw_message.get(last_offset as usize..) + { + message.extend_from_slice(bytes); + } + last_offset = 0; + } + current_message = prev_message; + } + StackItem::Boundary(prev_boundary) => { + if !current_boundary.is_empty() { + message.extend_from_slice(b"\r\n--"); + message.extend_from_slice(current_boundary.as_bytes()); + message.extend_from_slice(b"--\r\n"); + } + current_boundary = prev_boundary; + } + StackItem::None => { + message.extend_from_slice( + ¤t_message.raw_message + [last_offset as usize..prev_part.offset_end as usize], + ); + last_offset = prev_part.offset_end; + } + } + iter = prev_iter; + } else { + break; + } + } + + if last_offset > 0 + && let Some(bytes) = current_message.raw_message.get(last_offset as usize..) + { + message.extend_from_slice(bytes); + } + + message + } +} + +#[cfg(test)] +thread_local!(static COUNTER: std::cell::Cell = 0.into()); + +#[cfg(test)] +pub(crate) fn make_test_boundary() -> String { + format!("boundary_{}", COUNTER.with(|c| { c.replace(c.get() + 1) })) +} + +#[cfg(test)] +pub(crate) fn reset_test_boundary() { + COUNTER.with(|c| c.replace(0)); +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_notify.rs b/vendor/sieve-rs/src/runtime/actions/action_notify.rs new file mode 100644 index 0000000..718fde0 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_notify.rs @@ -0,0 +1,575 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_builder::headers::{date::Date, message_id::generate_message_id_header}; +use mail_parser::{HeaderName, decoders::quoted_printable::HEX_MAP}; + +use crate::{ + Context, Event, Importance, Recipient, + compiler::grammar::actions::{ + action_notify::Notify, + action_redirect::{ByTime, Ret}, + }, +}; + +use super::action_vacation::MAX_SUBJECT_LEN; + +impl Notify { + pub(crate) fn exec(&self, ctx: &mut Context) { + // Do not notify on Auto-Submitted messages + for header in &ctx.message.parts[0].headers { + if matches!(&header.name, HeaderName::Other(name) if name.eq_ignore_ascii_case("Auto-Submitted")) + && header + .value + .as_text() + .is_none_or(|v| !v.eq_ignore_ascii_case("no")) + { + return; + } + } + + let uri = ctx.eval_value(&self.method).to_string().into_owned(); + let (scheme, params) = if let Some(parts) = parse_uri(&uri) { + parts + } else { + return; + }; + + let has_fcc = self.fcc.is_some(); + let is_mailto = scheme.eq_ignore_ascii_case("mailto") + && ctx.num_out_messages < ctx.runtime.max_out_messages; + let mut events = Vec::with_capacity(3); + + if is_mailto || has_fcc { + let params = if is_mailto { + if let Some(params) = parse_mailto(params) { + params + } else { + return; + } + } else { + MailtoMessage { + to: Vec::new(), + cc: Vec::new(), + bcc: Vec::new(), + body: None, + headers: Vec::new(), + } + }; + let from = if let Some(from) = &self.from { + let from = ctx.eval_value(from).to_string().into_owned(); + if from + .to_ascii_lowercase() + .contains(&ctx.user_address.to_ascii_lowercase()) + { + from + } else { + ctx.user_from_field() + } + } else { + ctx.user_from_field() + }; + let notify_message = self + .message + .as_ref() + .map(|m| ctx.eval_value(m).to_string().into_owned()); + let message_len = params + .to + .iter() + .chain(params.cc.iter()) + .map(|a| a.len() + 4) + .sum::() + + params + .headers + .iter() + .map(|(h, v)| h.len() + v.len() + 4) + .sum::() + + params.body.as_ref().map_or(0, |b| b.len()) + + notify_message.as_ref().map_or(0, |b| b.len()) + + from.len() + + 200; + + let mut message = Vec::with_capacity(message_len); + message.extend_from_slice(b"From: "); + message.extend_from_slice(from.as_bytes()); + message.extend_from_slice(b"\r\n"); + + for (header, addresses) in [("To: ", ¶ms.to), ("Cc: ", ¶ms.cc)] { + if !addresses.is_empty() { + message.extend_from_slice(header.as_bytes()); + for (pos, address) in addresses.iter().enumerate() { + if pos > 0 { + message.extend_from_slice(b", "); + } + if !address.contains('<') { + message.push(b'<'); + } + message.extend_from_slice(address.as_bytes()); + if !address.contains('<') { + message.push(b'>'); + } + } + message.extend_from_slice(b"\r\n"); + } + } + + let mut has_subject = None; + let mut has_date = false; + let mut has_message_id = false; + for (header, value) in ¶ms.headers { + match header { + HeaderName::Subject => { + has_subject = value.into(); + continue; + } + HeaderName::Date => { + has_date = true; + } + HeaderName::MessageId => { + has_message_id = true; + } + HeaderName::From => { + continue; + } + _ => (), + } + message.extend_from_slice(header.as_str().as_bytes()); + message.extend_from_slice(b": "); + message.extend_from_slice(value.as_bytes()); + message.extend_from_slice(b"\r\n"); + } + + if !has_date { + message.extend_from_slice(b"Date: "); + message.extend_from_slice(Date::now().to_rfc822().as_bytes()); + message.extend_from_slice(b"\r\n"); + } + + if !has_message_id { + message.extend_from_slice(b"Message-ID: "); + generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap(); + message.extend_from_slice(b"\r\n"); + } + + let (importance, priority) = + self.importance + .as_ref() + .map_or(("Normal", "3 (Normal)"), |i| { + match ctx.eval_value(i).to_string().as_ref() { + "1" => ("High", "1 (High)"), + "3" => ("Low", "5 (Low)"), + _ => ("Normal", "3 (Normal)"), + } + }); + message.extend_from_slice(b"Importance: "); + message.extend_from_slice(importance.as_bytes()); + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"X-Priority: "); + message.extend_from_slice(priority.as_bytes()); + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"Subject: "); + let subject = if let Some(subject) = has_subject { + subject.as_str() + } else if let Some(subject) = ¬ify_message { + subject.as_ref() + } else { + ctx.message.subject().unwrap_or_default() + }; + let mut iter = subject.chars().enumerate(); + let mut buf = [0; 4]; + #[allow(clippy::while_let_on_iterator)] + while let Some((pos, char)) = iter.next() { + if pos < MAX_SUBJECT_LEN { + message.extend_from_slice(char.encode_utf8(&mut buf).as_bytes()); + } else { + break; + } + } + if iter.next().is_some() { + message.extend_from_slice('…'.encode_utf8(&mut buf).as_bytes()); + } + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"Auto-Submitted: auto-notified\r\n"); + message.extend_from_slice(b"X-Sieve: yes\r\n"); + message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n"); + if let Some(body) = params.body { + message.extend_from_slice(body.as_bytes()); + } else if let Some(subject) = ¬ify_message { + message.extend_from_slice(subject.as_bytes()); + } else if let Some(subject) = ctx.message.subject() { + message.extend_from_slice(subject.as_bytes()); + } + + ctx.last_message_id += 1; + events.push(Event::CreatedMessage { + message_id: ctx.last_message_id, + message, + }); + + if is_mailto { + events.push(Event::SendMessage { + recipient: Recipient::Group( + params + .to + .into_iter() + .chain(params.cc) + .chain(params.bcc) + .map(|addr| { + if let Some((addr, _)) = addr + .rsplit_once('<') + .and_then(|(_, addr)| addr.rsplit_once('>')) + { + addr.to_string() + } else { + addr + } + }) + .collect(), + ), + notify: crate::compiler::grammar::actions::action_redirect::Notify::Never, + return_of_content: Ret::Default, + by_time: ByTime::None, + message_id: ctx.last_message_id, + }); + } + } + + if !is_mailto { + events.push(Event::Notify { + method: uri, + from: self + .from + .as_ref() + .map(|f| ctx.eval_value(f).to_string().into_owned()), + importance: self.importance.as_ref().map_or(Importance::Normal, |i| { + match ctx.eval_value(i).to_string().as_ref() { + "1" => Importance::High, + "3" => Importance::Low, + _ => Importance::Normal, + } + }), + options: ctx.eval_values_owned(&self.options), + message: self + .message + .as_ref() + .map(|m| ctx.eval_value(m).to_string().into_owned()) + .or_else(|| ctx.message.subject().map(|s| s.to_string())) + .unwrap_or_default(), + }); + ctx.num_out_messages += 1; + } + + if let Some(fcc) = &self.fcc { + // File carbon copy + events.push(Event::FileInto { + folder: ctx.eval_value(&fcc.mailbox).to_string().into_owned(), + flags: ctx.get_local_flags(&fcc.flags), + mailbox_id: fcc + .mailbox_id + .as_ref() + .map(|m| ctx.eval_value(m).to_string().into_owned()), + special_use: fcc + .special_use + .as_ref() + .map(|s| ctx.eval_value(s).to_string().into_owned()), + create: fcc.create, + message_id: ctx.last_message_id, + }); + } + ctx.queued_events = events.into_iter(); + } +} + +pub fn validate_from(addr: &str) -> bool { + let mut has_at = false; + let mut has_dot = false; + let mut in_quote = false; + let mut in_angle = false; + let mut last_ch = 0; + + for &ch in addr.as_bytes().iter() { + match ch { + b'\"' if last_ch != b'\\' => { + in_quote = !in_quote; + } + b'<' if !in_quote => { + if !in_angle { + in_angle = true; + has_at = false; + has_dot = false; + } else { + return false; + } + } + b'>' if !in_quote => { + if in_angle { + in_angle = false; + } else { + return false; + } + } + b'@' if !in_quote => { + if !has_at && last_ch.is_ascii_alphanumeric() { + has_at = true; + } else { + return false; + } + } + b'.' if !in_quote && has_at => { + has_dot = true; + } + _ => (), + } + last_ch = ch; + } + + has_dot && has_at && !in_angle +} + +pub fn validate_uri(uri: &str) -> Option<&str> { + let (scheme, uri) = parse_uri(uri)?; + if scheme.eq_ignore_ascii_case("mailto") { + parse_mailto(uri)?; + scheme.into() + } else if ["xmpp", "tel", "http", "https"].contains(&scheme) { + scheme.into() + } else { + None + } +} + +pub(crate) fn parse_uri(uri: &str) -> Option<(&str, &str)> { + let (scheme, uri) = uri.split_once(':')?; + + if !uri.is_empty() { + Some((scheme, uri)) + } else { + None + } +} + +pub enum Mailto { + Header(HeaderName<'static>), + Body, + Other(String), +} + +enum State { + Address((HeaderName<'static>, bool)), + ParamName, + ParamValue(Mailto), +} + +#[derive(Default)] +struct MailtoMessage { + to: Vec, + cc: Vec, + bcc: Vec, + body: Option, + headers: Vec<(HeaderName<'static>, String)>, +} + +fn parse_mailto(uri: &str) -> Option { + let mut params = MailtoMessage::default(); + + let mut state = State::Address((HeaderName::To, false)); + let mut buf = Vec::new(); + let uri_ = uri.as_bytes(); + let mut iter = uri_.iter(); + let mut has_addresses = false; + + while let Some(&ch) = iter.next() { + match ch { + b'%' => { + let hex1 = HEX_MAP[*iter.next()? as usize]; + let hex2 = HEX_MAP[*iter.next()? as usize]; + if hex1 != -1 && hex2 != -1 { + let ch = ((hex1 as u8) << 4) | hex2 as u8; + + match &state { + State::Address((header, has_at)) => match ch { + b',' => { + if *has_at { + insert_address( + &mut params, + header.clone(), + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + has_addresses = true; + state = State::Address((header.clone(), false)); + } else { + return None; + } + } + b'@' => { + if !*has_at { + state = State::Address((header.clone(), true)); + buf.push(ch); + } else { + return None; + } + } + _ => { + buf.push(ch); + } + }, + _ => buf.push(ch), + } + } else { + return None; + } + } + b',' => match &state { + State::Address((header, true)) => { + insert_address( + &mut params, + header.clone(), + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + state = State::Address((header.clone(), false)); + has_addresses = true; + } + State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'?' => match &state { + State::Address((header, has_at)) if *has_at || buf.is_empty() => { + if !buf.is_empty() { + insert_address( + &mut params, + header.clone(), + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + has_addresses = true; + } + state = State::ParamName; + } + State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'@' => match &state { + State::Address((header, false)) if !buf.is_empty() => { + buf.push(ch); + state = State::Address((header.clone(), true)); + } + State::ParamName | State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'=' => match &state { + State::ParamName if !buf.is_empty() => { + let param = String::from_utf8(std::mem::take(&mut buf)).ok()?; + state = HeaderName::parse(param) + .map(|hdr| match hdr { + HeaderName::To | HeaderName::Cc | HeaderName::Bcc => { + State::Address((hdr, false)) + } + HeaderName::Other(param) => { + if param.eq_ignore_ascii_case("body") { + State::ParamValue(Mailto::Body) + } else { + State::ParamValue(Mailto::Other(param.into_owned())) + } + } + _ => State::ParamValue(Mailto::Header(hdr)), + }) + .unwrap_or_else(|| State::ParamValue(Mailto::Other(String::new()))); + } + State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'&' => match state { + State::Address((header, true)) => { + if !buf.is_empty() { + insert_address( + &mut params, + header, + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + } + state = State::ParamName; + } + State::ParamValue(param) => { + if !buf.is_empty() { + let value = String::from_utf8(std::mem::take(&mut buf)).ok()?; + match param { + Mailto::Header(header) => params.headers.push((header, value)), + Mailto::Body => params.body = value.into(), + Mailto::Other(header) => params.headers.push((header.into(), value)), + } + } + state = State::ParamName; + } + _ => return None, + }, + _ => match &state { + State::ParamName => { + if ch.is_ascii_alphanumeric() || b"-_".contains(&ch) { + buf.push(ch); + } else { + return None; + } + } + _ => { + if !ch.is_ascii_whitespace() { + buf.push(ch); + } + } + }, + } + } + + if !buf.is_empty() { + let value = String::from_utf8(std::mem::take(&mut buf)).ok()?; + match state { + State::Address((header, true)) => { + insert_address(&mut params, header, value); + has_addresses = true; + } + State::ParamName => { + params + .headers + .push((HeaderName::Other(value.into()), String::new())); + } + State::ParamValue(param) => match param { + Mailto::Header(header) => params.headers.push((header, value)), + Mailto::Body => params.body = value.into(), + Mailto::Other(header) => params + .headers + .push((HeaderName::Other(header.into()), value)), + }, + _ => return None, + } + } + + if has_addresses { Some(params) } else { None } +} + +#[inline(always)] +fn insert_address(params: &mut MailtoMessage, name: HeaderName, value: String) { + if !params + .to + .iter() + .chain(params.cc.iter()) + .chain(params.bcc.iter()) + .any(|v| v.eq_ignore_ascii_case(&value)) + { + match name { + HeaderName::To => { + params.to.push(value); + } + HeaderName::Cc => { + params.cc.push(value); + } + HeaderName::Bcc => { + params.bcc.push(value); + } + _ => (), + } + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_redirect.rs b/vendor/sieve-rs/src/runtime/actions/action_redirect.rs new file mode 100644 index 0000000..10a0ce0 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_redirect.rs @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use mail_parser::{DateTime, HeaderName}; + +use crate::{ + Context, Event, Recipient, + compiler::grammar::actions::action_redirect::{ByTime, Redirect}, +}; + +impl Redirect { + pub(crate) fn exec(&self, ctx: &mut Context) { + if let Some(address) = sanitize_address(ctx.eval_value(&self.address).to_string().as_ref()) + && ctx.num_redirects < ctx.runtime.max_redirects + && ctx.num_out_messages < ctx.runtime.max_out_messages + && ctx.message.parts[0] + .headers + .iter() + .filter(|h| matches!(&h.name, HeaderName::Received)) + .count() + < ctx.runtime.max_received_headers + { + // Try to avoid forwarding loops + if !self.list && address.eq_ignore_ascii_case(ctx.user_address.as_ref()) { + return; + } + + if !self.copy && matches!(&ctx.final_event, Some(Event::Keep { .. })) { + ctx.final_event = None; + } + + let mut events = Vec::with_capacity(2); + if let Some(event) = ctx.build_message_id() { + events.push(event); + } + ctx.num_redirects += 1; + ctx.num_out_messages += 1; + events.push(Event::SendMessage { + recipient: if !self.list { + Recipient::Address(address) + } else { + Recipient::List(address) + }, + notify: self.notify.clone(), + return_of_content: self.return_of_content.clone(), + by_time: match &self.by_time { + ByTime::Relative { + rlimit, + mode, + trace, + } => ByTime::Relative { + rlimit: *rlimit, + mode: mode.clone(), + trace: *trace, + }, + ByTime::Absolute { + alimit, + mode, + trace, + } => ByTime::Absolute { + alimit: DateTime::parse_rfc3339( + ctx.eval_value(alimit).to_string().as_ref(), + ) + .and_then(|d| { + if d.is_valid() { + d.to_timestamp().into() + } else { + None + } + }) + .unwrap_or(0), + mode: mode.clone(), + trace: *trace, + }, + ByTime::None => ByTime::None, + }, + message_id: ctx.main_message_id, + }); + ctx.queued_events = events.into_iter(); + } + } +} + +pub(crate) fn sanitize_address(addr: &str) -> Option { + let mut result = String::with_capacity(addr.len()); + let mut in_quote = false; + let mut last_ch = '\n'; + let mut has_at = false; + let mut has_dot = false; + + for ch in addr.chars() { + match ch { + '\"' => { + if !in_quote { + in_quote = true; + } else if last_ch != '\\' { + in_quote = false; + } + } + '@' if !in_quote => { + if !has_at && !result.is_empty() { + has_at = true; + result.push(ch); + } else { + return None; + } + } + '.' if !in_quote && has_at && !has_dot => { + has_dot = true; + result.push(ch); + } + '<' => { + result.clear(); + has_at = false; + has_dot = false; + } + '>' => (), + _ => { + if !ch.is_ascii_whitespace() || in_quote { + result.push(ch); + } + } + } + last_ch = ch; + } + + if !result.is_empty() && has_at && has_dot { + Some(result) + } else { + None + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_set.rs b/vendor/sieve-rs/src/runtime/actions/action_set.rs new file mode 100644 index 0000000..a9c3996 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_set.rs @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + Context, Envelope, Event, + compiler::{ + VariableType, + grammar::actions::action_set::{Modifier, Set}, + }, + runtime::Variable, +}; +use std::fmt::Write; + +impl Set { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut value = ctx.eval_value(&self.value); + for modifier in &self.modifiers { + value = modifier.apply(value.to_string().as_ref(), ctx).into(); + } + + ctx.set_variable(&self.name, value); + } +} + +impl Context<'_> { + pub(crate) fn set_variable(&mut self, var_name: &VariableType, mut variable: Variable) { + if variable.len() > self.runtime.max_variable_size { + let mut new_variable = String::with_capacity(self.runtime.max_variable_size); + for ch in variable.to_string().chars() { + if ch.len_utf8() + new_variable.len() <= self.runtime.max_variable_size { + new_variable.push(ch); + } else { + break; + } + } + variable = new_variable.into(); + } + + match var_name { + VariableType::Local(var_id) => { + if let Some(var) = self.vars_local.get_mut(*var_id) { + *var = variable.clone(); + } else { + debug_assert!(false, "Non-existent local variable {var_id}"); + } + } + VariableType::Global(var_name) => { + self.vars_global + .insert(var_name.to_string().into(), variable.clone()); + } + VariableType::Envelope(env) => { + self.add_set_envelope_event(*env, variable.to_string().into_owned()); + } + _ => (), + } + } + + pub(crate) fn add_set_envelope_event(&mut self, envelope: Envelope, value: String) { + let mut did_find = false; + for (name, val) in self.envelope.iter_mut() { + if *name == envelope { + *val = Variable::String(value.clone().into()); + did_find = true; + break; + } + } + if !did_find { + self.envelope + .push((envelope, Variable::String(value.clone().into()))); + } + self.queued_events = vec![Event::SetEnvelope { envelope, value }].into_iter(); + } + + pub(crate) fn get_variable(&self, var_name: &VariableType) -> Option<&Variable> { + match var_name { + VariableType::Local(var_id) => self.vars_local.get(*var_id), + VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()), + VariableType::Envelope(env) => self + .envelope + .iter() + .find_map(|(name, val)| if name == env { Some(val) } else { None }), + _ => unreachable!(), + } + } +} + +impl Modifier { + pub(crate) fn apply(&self, input: &str, ctx: &Context) -> String { + let max_len = ctx.runtime.max_variable_size; + match self { + Modifier::Lower => input.to_lowercase(), + Modifier::Upper => input.to_uppercase(), + Modifier::LowerFirst => { + let mut result = String::with_capacity(input.len()); + for (pos, char) in input.chars().enumerate() { + if result.len() + char.len_utf8() <= max_len { + if pos != 0 { + result.push(char); + } else { + for char in char.to_lowercase() { + result.push(char); + } + } + } else { + return result; + } + } + result + } + Modifier::UpperFirst => { + let mut result = String::with_capacity(input.len()); + for (pos, char) in input.chars().enumerate() { + if result.len() + char.len_utf8() <= max_len { + if pos != 0 { + result.push(char); + } else { + for char in char.to_uppercase() { + result.push(char); + } + } + } else { + return result; + } + } + result + } + Modifier::QuoteWildcard => { + let mut result = String::with_capacity(input.len()); + for char in input.chars() { + if ['*', '\\', '?'].contains(&char) { + if result.len() + char.len_utf8() < max_len { + result.push('\\'); + result.push(char); + } else { + return result; + } + } else if result.len() + char.len_utf8() <= max_len { + result.push(char); + } else { + return result; + } + } + result + } + Modifier::QuoteRegex => { + let mut result = String::with_capacity(input.len()); + for char in input.chars() { + if [ + '*', '\\', '?', '.', '[', ']', '(', ')', '+', '{', '}', '|', '^', '=', ':', + '$', + ] + .contains(&char) + { + if result.len() + char.len_utf8() < max_len { + result.push('\\'); + result.push(char); + } else { + return result; + } + } else if result.len() + char.len_utf8() <= max_len { + result.push(char); + } else { + return result; + } + } + result + } + Modifier::Length => input.chars().count().to_string(), + Modifier::EncodeUrl => { + let mut buf = [0; 4]; + let mut result = String::with_capacity(input.len()); + + for char in input.chars() { + if char.is_ascii_alphanumeric() || ['-', '.', '_', '~'].contains(&char) { + if result.len() < max_len { + result.push(char); + } else { + return result; + } + } else if result.len() + (char.len_utf8() * 3) <= max_len { + for byte in char.encode_utf8(&mut buf).as_bytes().iter() { + write!(result, "%{byte:02x}").ok(); + } + } else { + return result; + } + } + result + } + Modifier::Replace { find, replace } => input.replace( + ctx.eval_value(find).to_string().as_ref(), + ctx.eval_value(replace).to_string().as_ref(), + ), + } + } +} diff --git a/vendor/sieve-rs/src/runtime/actions/action_vacation.rs b/vendor/sieve-rs/src/runtime/actions/action_vacation.rs new file mode 100644 index 0000000..db75787 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/action_vacation.rs @@ -0,0 +1,336 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::borrow::Cow; + +use mail_builder::headers::{date::Date, message_id::generate_message_id_header}; +use mail_parser::{HeaderName, HeaderValue}; + +use crate::{ + Context, Envelope, Event, Recipient, + compiler::grammar::{ + AddressPart, + actions::{ + action_redirect::{ByTime, Notify, Ret}, + action_vacation::{Period, TestVacation, Vacation}, + }, + }, + runtime::tests::TestResult, +}; + +pub(crate) const MAX_SUBJECT_LEN: usize = 256; + +impl TestVacation { + pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult { + let mut from = String::new(); + let mut user_addresses = Vec::new(); + + if ctx.num_out_messages >= ctx.runtime.max_out_messages { + return TestResult::Bool(false); + } + + for (name, value) in &ctx.envelope { + if !value.is_empty() { + match name { + Envelope::From => { + from = value.to_string().to_ascii_lowercase(); + } + Envelope::To if !ctx.runtime.vacation_use_orig_rcpt => { + user_addresses.push(value.to_string()); + } + Envelope::Orcpt if ctx.runtime.vacation_use_orig_rcpt => { + user_addresses.push(value.to_string()); + } + _ => (), + } + } + } + + // Add user specified addresses + for address in &self.addresses { + let address = ctx.eval_value(address).to_string().into_owned(); + if !address.is_empty() { + user_addresses.push(address.into()); + } + } + if !ctx.user_address.is_empty() { + user_addresses.push(ctx.user_address.as_ref().into()); + } + + // Do not reply to own address + if from.is_empty() + || user_addresses.is_empty() + || from.starts_with("mailer-daemon") + || from.starts_with("owner-") + || from.contains("-request@") + || user_addresses.iter().any(|a| a.eq_ignore_ascii_case(&from)) + { + return TestResult::Bool(false); + } + + // Check headers + let mut found_rcpt = false; + let mut received_count = 0; + for header in &ctx.message.parts[0].headers { + match &header.name { + HeaderName::To + | HeaderName::Cc + | HeaderName::Bcc + | HeaderName::ResentTo + | HeaderName::ResentBcc + | HeaderName::ResentCc + if !found_rcpt => + { + found_rcpt = ctx.find_addresses(header, &AddressPart::All, |addr| { + user_addresses.iter().any(|a| a.eq_ignore_ascii_case(addr)) + }); + } + HeaderName::ListArchive + | HeaderName::ListHelp + | HeaderName::ListId + | HeaderName::ListOwner + | HeaderName::ListPost + | HeaderName::ListSubscribe + | HeaderName::ListUnsubscribe => { + // Do not send vacation responses to lists + return TestResult::Bool(false); + } + HeaderName::Received => { + received_count += 1; + } + HeaderName::Other(header_name) => { + if header_name.eq_ignore_ascii_case("Auto-Submitted") { + if header + .value + .as_text() + .is_none_or(|v| !v.eq_ignore_ascii_case("no")) + { + return TestResult::Bool(false); + } + } else if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") { + if header.value.as_text().is_some_and(|v| { + v.to_ascii_lowercase() + .split(',') + .any(|v| ["all", "oof"].contains(&v.trim())) + }) { + return TestResult::Bool(false); + } + } else if header_name.eq_ignore_ascii_case("Precedence") + && header + .value + .as_text() + .is_some_and(|v| v.eq_ignore_ascii_case("bulk")) + { + return TestResult::Bool(false); + } + } + _ => (), + } + } + + // No user address found in header or possible loop + if found_rcpt && received_count <= ctx.runtime.max_received_headers { + TestResult::Event { + event: Event::DuplicateId { + id: if let Some(handle) = &self.handle { + format!("_v{}{}", from, ctx.eval_value(handle).to_string()) + } else { + format!("_v{}{}", from, ctx.eval_value(&self.reason).to_string()) + }, + expiry: match &self.period { + Period::Days(days) => days * 86400, + Period::Seconds(seconds) => *seconds, + Period::Default => ctx.runtime.default_vacation_expiry, + }, + last: false, + }, + is_not: true, + } + } else { + TestResult::Bool(false) + } + } +} + +impl Vacation { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut vacation_to = Cow::from(""); + + for (name, value) in &ctx.envelope { + if !value.is_empty() && name == &Envelope::From { + vacation_to = value.to_string(); + break; + } + } + + // Check headers + let mut vacation_subject = if let Some(subject) = &self.subject { + ctx.eval_value(subject) + } else { + "".into() + }; + + // Check headers + let mut message_id = None; + let mut vacation_to_full = None; + let mut references = None; + for header in &ctx.message.parts[0].headers { + match &header.name { + HeaderName::Subject if vacation_subject.is_empty() => { + if let Some(subject) = header.value.as_text() { + let mut vacation_subject_ = String::with_capacity(MAX_SUBJECT_LEN); + let mut iter = ctx + .runtime + .vacation_subject_prefix + .chars() + .chain(subject.chars()) + .enumerate(); + + #[allow(clippy::while_let_on_iterator)] + while let Some((pos, char)) = iter.next() { + if pos < MAX_SUBJECT_LEN { + vacation_subject_.push(char); + } else { + break; + } + } + if iter.next().is_some() { + vacation_subject_.push('…'); + } + vacation_subject = vacation_subject_.into(); + } + } + HeaderName::MessageId => { + message_id = header.value.as_text(); + } + HeaderName::References if header.offset_start > 0 => { + references = (&ctx.message.raw_message + [header.offset_start as usize..header.offset_end as usize]) + .into(); + } + HeaderName::From | HeaderName::Sender + if matches!(&header.value, HeaderValue::Address(address) if address.contains(vacation_to.as_ref())) + && header.offset_start > 0 => + { + vacation_to_full = (&ctx.message.raw_message + [header.offset_start as usize..header.offset_end as usize]) + .into(); + } + _ => (), + } + } + + // Build message + let vacation_from = if let Some(from) = &self.from { + ctx.eval_value(from) + } else if !ctx.user_address.is_empty() { + ctx.user_from_field().into() + } else if let Some(addr) = ctx + .envelope + .iter() + .find_map(|(n, v)| if n == &Envelope::To { Some(v) } else { None }) + { + addr.to_string().into() + } else { + "".into() + }; + if vacation_subject.is_empty() { + vacation_subject = ctx.runtime.vacation_default_subject.as_ref().into(); + } + let vacation_body = ctx.eval_value(&self.reason); + let message_len = vacation_body.len() + + vacation_from.len() + + vacation_to_full + .as_ref() + .map_or(vacation_to.len(), |t| t.len()) + + vacation_subject.len() + + message_id.as_ref().map_or(0, |m| m.len() * 2) + + references.as_ref().map_or(0, |m| m.len()) + + 160; + + let mut message = Vec::with_capacity(message_len); + write_header(&mut message, "From: ", vacation_from.to_string().as_ref()); + if let Some(vacation_to_full) = vacation_to_full { + message.extend_from_slice(b"To:"); + message.extend_from_slice(vacation_to_full); + } else { + write_header(&mut message, "To: ", vacation_to.to_string().as_ref()); + } + write_header( + &mut message, + "Subject: ", + vacation_subject.to_string().as_ref(), + ); + if let Some(message_id) = message_id { + message.extend_from_slice(b"In-Reply-To: <"); + message.extend_from_slice(message_id.as_bytes()); + message.extend_from_slice(b">\r\n"); + + message.extend_from_slice(b"References: <"); + message.extend_from_slice(message_id.as_bytes()); + if let Some(references) = references { + message.extend_from_slice(b"> "); + message.extend_from_slice(references); + } else { + message.extend_from_slice(b">\r\n"); + } + } + message.extend_from_slice(b"Date: "); + message.extend_from_slice(Date::now().to_rfc822().as_bytes()); + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"Message-ID: "); + generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap(); + message.extend_from_slice(b"\r\n"); + + write_header(&mut message, "Auto-Submitted: ", "auto-replied"); + if !self.mime { + message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n"); + } + message.extend_from_slice(vacation_body.to_string().as_bytes()); + + // Add action + let mut events = Vec::with_capacity(3); + ctx.last_message_id += 1; + ctx.num_out_messages += 1; + events.push(Event::CreatedMessage { + message_id: ctx.last_message_id, + message, + }); + events.push(Event::SendMessage { + recipient: Recipient::Address(vacation_to.to_string()), + notify: Notify::Never, + return_of_content: Ret::Default, + by_time: ByTime::None, + message_id: ctx.last_message_id, + }); + + // File carbon copy + if let Some(fcc) = &self.fcc { + events.push(Event::FileInto { + folder: ctx.eval_value(&fcc.mailbox).to_string().into_owned(), + flags: ctx.get_local_flags(&fcc.flags), + mailbox_id: fcc + .mailbox_id + .as_ref() + .map(|m| ctx.eval_value(m).to_string().into_owned()), + special_use: fcc + .special_use + .as_ref() + .map(|s| ctx.eval_value(s).to_string().into_owned()), + create: fcc.create, + message_id: ctx.last_message_id, + }); + } + ctx.queued_events = events.into_iter(); + } +} + +fn write_header(buf: &mut Vec, name: &str, value: &str) { + buf.extend_from_slice(name.as_bytes()); + buf.extend_from_slice(value.as_bytes()); + buf.extend_from_slice(b"\r\n"); +} diff --git a/vendor/sieve-rs/src/runtime/actions/mod.rs b/vendor/sieve-rs/src/runtime/actions/mod.rs new file mode 100644 index 0000000..70f3c34 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/actions/mod.rs @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod action_convert; +pub mod action_editheader; +pub mod action_fileinto; +pub mod action_flags; +pub mod action_include; +pub mod action_mime; +pub mod action_notify; +pub mod action_redirect; +pub mod action_set; +pub mod action_vacation; diff --git a/vendor/sieve-rs/src/runtime/context.rs b/vendor/sieve-rs/src/runtime/context.rs new file mode 100644 index 0000000..d48fcf5 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/context.rs @@ -0,0 +1,689 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{borrow::Cow, sync::Arc, time::SystemTime}; + +use ahash::AHashMap; +use mail_parser::Message; + +use crate::{ + Context, Envelope, Event, Input, MAX_LOCAL_VARIABLES, MAX_MATCH_VARIABLES, Metadata, Runtime, + Sieve, SpamStatus, VirusStatus, + compiler::grammar::{Capability, instruction::Instruction}, +}; + +use super::{ + RuntimeError, Variable, + actions::action_include::IncludeResult, + tests::{TestResult, test_envelope::parse_envelope_address}, +}; + +#[derive(Clone, Debug)] +pub(crate) struct ScriptStack { + pub(crate) script: Arc, + pub(crate) prev_pos: usize, + pub(crate) prev_vars_local: Vec, + pub(crate) prev_vars_match: Vec, +} + +impl<'x> Context<'x> { + #[cfg(not(test))] + pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self { + Context { + #[cfg(test)] + runtime: runtime.clone(), + #[cfg(not(test))] + runtime, + message, + part: 0, + part_iter: Vec::new().into_iter(), + part_iter_stack: Vec::new(), + pos: usize::MAX, + test_result: false, + script_cache: AHashMap::new(), + script_stack: Vec::with_capacity(0), + vars_global: AHashMap::new(), + vars_env: AHashMap::new(), + vars_local: Vec::with_capacity(0), + vars_match: Vec::with_capacity(0), + expr_stack: Vec::with_capacity(16), + expr_pos: 0, + envelope: Vec::new(), + metadata: Vec::new(), + message_size: usize::MAX, + final_event: Event::Keep { + flags: Vec::with_capacity(0), + message_id: 0, + } + .into(), + queued_events: vec![].into_iter(), + has_changes: false, + user_address: "".into(), + user_full_name: "".into(), + current_time: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) as i64, + num_redirects: 0, + num_instructions: 0, + num_out_messages: 0, + last_message_id: 0, + main_message_id: 0, + virus_status: VirusStatus::Unknown, + spam_status: SpamStatus::Unknown, + } + } + + #[allow(clippy::while_let_on_iterator)] + pub fn run(&mut self, input: Input) -> Option> { + match input { + Input::True => self.test_result ^= true, + Input::False => self.test_result ^= false, + Input::FncResult(result) => { + self.expr_stack.push(result); + } + Input::Script { name, script } => { + let num_vars = script.num_vars; + let num_match_vars = script.num_match_vars; + + if num_match_vars <= MAX_MATCH_VARIABLES && num_vars <= MAX_LOCAL_VARIABLES { + if self.message_size == usize::MAX { + self.message_size = self.message.raw_message.len(); + } + + self.script_cache.insert(name, script.clone()); + self.script_stack.push(ScriptStack { + script, + prev_pos: self.pos, + prev_vars_local: std::mem::replace( + &mut self.vars_local, + vec![Variable::default(); num_vars as usize], + ), + prev_vars_match: std::mem::replace( + &mut self.vars_match, + vec![Variable::default(); num_match_vars as usize], + ), + }); + self.pos = 0; + self.test_result = false; + } + } + } + + // Return any queued events + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + + let mut current_script = self.script_stack.last()?.script.clone(); + let mut iter = current_script.instructions.get(self.pos..)?.iter(); + + 'outer: loop { + while let Some(instruction) = iter.next() { + self.num_instructions += 1; + if self.num_instructions > self.runtime.cpu_limit { + self.finish_loop(); + return Some(Err(RuntimeError::CPULimitReached)); + } + self.pos += 1; + + match instruction { + Instruction::Jz(jmp_pos) => { + if !self.test_result { + debug_assert!(*jmp_pos > self.pos - 1); + self.pos = *jmp_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + } + Instruction::Jnz(jmp_pos) => { + if self.test_result { + debug_assert!(*jmp_pos > self.pos - 1); + self.pos = *jmp_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + } + Instruction::Jmp(jmp_pos) => { + debug_assert_ne!(*jmp_pos, self.pos - 1); + self.pos = *jmp_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + Instruction::Test(test) => match test.exec(self) { + TestResult::Bool(result) => { + self.test_result = result; + } + TestResult::Event { event, is_not } => { + self.test_result = is_not; + return Some(Ok(event)); + } + TestResult::Error(err) => { + self.finish_loop(); + return Some(Err(err)); + } + }, + Instruction::Eval(expr) => match self.eval_expression(expr) { + Ok(result) => { + self.test_result = result.to_bool(); + } + Err(event) => { + return Some(Ok(event)); + } + }, + Instruction::Clear(clear) => { + if clear.local_vars_num > 0 { + if let Some(local_vars) = self.vars_local.get_mut( + clear.local_vars_idx as usize + ..(clear.local_vars_idx + clear.local_vars_num) as usize, + ) { + for local_var in local_vars.iter_mut() { + if !local_var.is_empty() { + *local_var = Variable::default(); + } + } + } else { + debug_assert!(false, "Failed to clear local variables: {clear:?}"); + } + } + if clear.match_vars != 0 { + self.clear_match_variables(clear.match_vars); + } + } + Instruction::Keep(keep) => { + let next_event = self.build_message_id(); + self.final_event = Event::Keep { + flags: self.get_local_or_global_flags(&keep.flags), + message_id: self.main_message_id, + } + .into(); + if let Some(next_event) = next_event { + return Some(Ok(next_event)); + } + } + Instruction::FileInto(fi) => { + fi.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Redirect(redirect) => { + redirect.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Discard => { + self.final_event = Event::Discard.into(); + } + Instruction::Stop => { + self.script_stack.clear(); + break 'outer; + } + Instruction::Reject(reject) => { + self.final_event = None; + return Some(Ok(Event::Reject { + extended: reject.ereject, + reason: self.eval_value(&reject.reason).to_string().into_owned(), + })); + } + Instruction::ForEveryPart(fep) => { + if let Some(next_part) = self.part_iter.next() { + self.part = next_part; + } else if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop() + { + debug_assert!(fep.jz_pos > self.pos - 1); + self.part_iter = prev_part_iter; + self.part = prev_part; + self.pos = fep.jz_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } else { + self.part = 0; + #[cfg(test)] + panic!("ForEveryPart executed without items on stack."); + } + } + Instruction::ForEveryPartPush => { + let part_iter = self + .find_nested_parts_ids(self.part_iter_stack.is_empty()) + .into_iter(); + self.part_iter_stack + .push((self.part, std::mem::replace(&mut self.part_iter, part_iter))); + } + Instruction::ForEveryPartPop(num_pops) => { + debug_assert!( + *num_pops > 0 && *num_pops <= self.part_iter_stack.len(), + "Pop out of range: {} with {} items.", + num_pops, + self.part_iter_stack.len() + ); + for _ in 0..*num_pops { + if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop() { + self.part_iter = prev_part_iter; + self.part = prev_part; + } else { + break; + } + } + } + Instruction::While(while_) => match self.eval_expression(&while_.expr) { + Ok(result) => { + if !result.to_bool() { + debug_assert!(while_.jz_pos > self.pos - 1); + self.pos = while_.jz_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + } + Err(event) => { + return Some(Ok(event)); + } + }, + Instruction::Let(let_) => match self.eval_expression(&let_.expr) { + Ok(result) => { + self.set_variable(&let_.name, result); + } + Err(event) => { + return Some(Ok(event)); + } + }, + + Instruction::Replace(replace) => replace.exec(self), + Instruction::Enclose(enclose) => enclose.exec(self), + Instruction::ExtractText(extract) => { + extract.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::AddHeader(add_header) => add_header.exec(self), + Instruction::DeleteHeader(delete_header) => delete_header.exec(self), + Instruction::Set(set) => { + set.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Notify(notify) => { + notify.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Vacation(vacation) => { + vacation.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::EditFlags(flags) => flags.exec(self), + Instruction::Include(include) => match include.exec(self) { + IncludeResult::Cached(script) => { + self.script_stack.push(ScriptStack { + script: script.clone(), + prev_pos: self.pos, + prev_vars_local: std::mem::replace( + &mut self.vars_local, + vec![Variable::default(); script.num_vars as usize], + ), + prev_vars_match: std::mem::replace( + &mut self.vars_match, + vec![Variable::default(); script.num_match_vars as usize], + ), + }); + self.pos = 0; + current_script = script; + iter = current_script.instructions.iter(); + continue; + } + IncludeResult::Event(event) => { + return Some(Ok(event)); + } + IncludeResult::Error(err) => { + self.finish_loop(); + return Some(Err(err)); + } + IncludeResult::None => (), + }, + Instruction::Convert(convert) => { + convert.exec(self); + } + Instruction::Return => { + break; + } + Instruction::Require(capabilities) => { + for capability in capabilities { + if !self.runtime.allowed_capabilities.contains(capability) { + self.finish_loop(); + return Some(Err( + if let Capability::Other(not_supported) = capability { + RuntimeError::CapabilityNotSupported(not_supported.clone()) + } else { + RuntimeError::CapabilityNotAllowed(capability.clone()) + }, + )); + } + } + } + Instruction::Error(err) => { + self.finish_loop(); + return Some(Err(RuntimeError::ScriptErrorMessage( + self.eval_value(&err.message).to_string().into_owned(), + ))); + } + Instruction::Invalid(invalid) => { + self.finish_loop(); + return Some(Err(RuntimeError::InvalidInstruction(invalid.clone()))); + } + #[cfg(test)] + Instruction::TestCmd(arguments) => { + return Some(Ok(Event::Function { + id: u32::MAX, + arguments: arguments + .iter() + .map(|s| self.eval_value(s).to_owned()) + .collect(), + })); + } + } + } + + if let Some(prev_script) = self.script_stack.pop() { + self.pos = prev_script.prev_pos; + self.vars_local = prev_script.prev_vars_local; + self.vars_match = prev_script.prev_vars_match; + } + + if let Some(script_stack) = self.script_stack.last() { + current_script = script_stack.script.clone(); + iter = current_script.instructions.get(self.pos..)?.iter(); + } else { + break; + } + } + + match self.final_event.take() { + Some(Event::Keep { + mut flags, + message_id, + }) => { + let create_event = if self.has_changes { + self.build_message_id() + } else { + None + }; + + let global_flags = self.get_global_flags(); + if flags.is_empty() && !global_flags.is_empty() { + flags = global_flags; + } + if let Some(create_event) = create_event { + self.queued_events = vec![ + create_event, + Event::Keep { + flags, + message_id: self.main_message_id, + }, + ] + .into_iter(); + self.queued_events.next().map(Ok) + } else { + Some(Ok(Event::Keep { flags, message_id })) + } + } + Some(event) => Some(Ok(event)), + _ => None, + } + } + + pub(crate) fn finish_loop(&mut self) { + self.script_stack.clear(); + if let Some(event) = self.final_event.take() { + self.queued_events = if let Event::Keep { + mut flags, + message_id, + } = event + { + let global_flags = self.get_global_flags(); + if flags.is_empty() && !global_flags.is_empty() { + flags = global_flags; + } + + if self.has_changes { + if let Some(event) = self.build_message_id() { + vec![ + event, + Event::Keep { + flags, + message_id: self.main_message_id, + }, + ] + } else { + vec![Event::Keep { flags, message_id }] + } + } else { + vec![Event::Keep { flags, message_id }] + } + } else { + vec![event] + } + .into_iter(); + } + } + + pub fn set_envelope( + &mut self, + envelope: impl TryInto, + value: impl Into>, + ) { + if let Ok(envelope) = envelope.try_into() { + if matches!(&envelope, Envelope::From | Envelope::To) { + let value: Cow = value.into(); + if let Some(value) = parse_envelope_address(value.as_ref()) { + self.envelope.push((envelope, value.to_string().into())); + } + } else { + self.envelope.push((envelope, Variable::from(value.into()))); + } + } + } + + pub fn with_vars_env(mut self, vars_env: AHashMap, Variable>) -> Self { + self.vars_env = vars_env; + self + } + + pub fn with_envelope_list(mut self, envelope: Vec<(Envelope, Variable)>) -> Self { + self.envelope = envelope; + self + } + + pub fn with_envelope( + mut self, + envelope: impl TryInto, + value: impl Into>, + ) -> Self { + self.set_envelope(envelope, value); + self + } + + pub fn clear_envelope(&mut self) { + self.envelope.clear() + } + + pub fn set_user_address(&mut self, from: impl Into>) { + self.user_address = from.into(); + } + + pub fn with_user_address(mut self, from: impl Into>) -> Self { + self.set_user_address(from); + self + } + + pub fn set_user_full_name(&mut self, name: &str) { + let mut name_ = String::with_capacity(name.len()); + for ch in name.chars() { + if ['\"', '\\'].contains(&ch) { + name_.push('\\'); + } + name_.push(ch); + } + self.user_full_name = name_.into(); + } + + pub fn with_user_full_name(mut self, name: &str) -> Self { + self.set_user_full_name(name); + self + } + + pub fn set_env_variable( + &mut self, + name: impl Into>, + value: impl Into, + ) { + self.vars_env.insert(name.into(), value.into()); + } + + pub fn with_env_variable( + mut self, + name: impl Into>, + value: impl Into, + ) -> Self { + self.set_env_variable(name, value); + self + } + + pub fn set_global_variable( + &mut self, + name: impl Into>, + value: impl Into, + ) { + self.vars_global.insert(name.into(), value.into()); + } + + pub fn with_global_variable( + mut self, + name: impl Into>, + value: impl Into, + ) -> Self { + self.set_global_variable(name, value); + self + } + + pub fn set_medatata( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.metadata.push((name.into(), value.into())); + } + + pub fn with_metadata( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_medatata(name, value); + self + } + + pub fn set_spam_status(&mut self, status: impl Into) { + self.spam_status = status.into(); + } + + pub fn with_spam_status(mut self, status: impl Into) -> Self { + self.set_spam_status(status); + self + } + + pub fn set_virus_status(&mut self, status: impl Into) { + self.virus_status = status.into(); + } + + pub fn with_virus_status(mut self, status: impl Into) -> Self { + self.set_virus_status(status); + self + } + + pub fn take_message(&mut self) -> Message<'x> { + std::mem::take(&mut self.message) + } + + pub fn has_message_changed(&self) -> bool { + self.main_message_id > 0 + } + + pub(crate) fn user_from_field(&self) -> String { + if !self.user_full_name.is_empty() { + format!("\"{}\" <{}>", self.user_full_name, self.user_address) + } else { + self.user_address.to_string() + } + } + + pub fn global_variable_names(&self) -> impl Iterator { + self.vars_global.keys().map(|k| k.as_ref()) + } + + pub fn global_variable(&self, name: &str) -> Option<&Variable> { + self.vars_global.get(name) + } + + pub fn message(&self) -> &Message<'x> { + &self.message + } + + pub fn part(&self) -> u32 { + self.part + } +} + +#[cfg(test)] +impl<'x> Context<'x> { + pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self { + Context { + runtime: runtime.clone(), + message, + part: 0, + part_iter: Vec::new().into_iter(), + part_iter_stack: Vec::new(), + pos: usize::MAX, + test_result: false, + script_cache: AHashMap::new(), + script_stack: Vec::with_capacity(0), + vars_global: AHashMap::new(), + vars_env: AHashMap::new(), + vars_local: Vec::with_capacity(0), + vars_match: Vec::with_capacity(0), + expr_stack: Vec::with_capacity(16), + expr_pos: 0, + envelope: Vec::new(), + metadata: Vec::new(), + message_size: usize::MAX, + final_event: Event::Keep { + flags: Vec::with_capacity(0), + message_id: 0, + } + .into(), + queued_events: vec![].into_iter(), + has_changes: false, + user_address: "".into(), + user_full_name: "".into(), + current_time: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) as i64, + num_redirects: 0, + num_instructions: 0, + num_out_messages: 0, + last_message_id: 0, + main_message_id: 0, + virus_status: VirusStatus::Unknown, + spam_status: SpamStatus::Unknown, + } + } +} diff --git a/vendor/sieve-rs/src/runtime/eval.rs b/vendor/sieve-rs/src/runtime/eval.rs new file mode 100644 index 0000000..135bfa7 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/eval.rs @@ -0,0 +1,482 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::cmp::Ordering; + +use mail_parser::{ + Addr, Header, HeaderName, HeaderValue, Host, PartType, Received, + decoders::html::{html_to_text, text_to_html}, + parsers::MessageStream, +}; + +use crate::{ + Context, + compiler::{ + ContentTypePart, HeaderPart, HeaderVariable, MessagePart, ReceivedHostname, ReceivedPart, + Value, VariableType, + }, +}; + +use super::Variable; + +impl<'x> Context<'x> { + pub(crate) fn variable<'y: 'x>(&'y self, var: &VariableType) -> Option { + match var { + VariableType::Local(var_num) => self.vars_local.get(*var_num).cloned(), + VariableType::Match(var_num) => self.vars_match.get(*var_num).cloned(), + VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()).cloned(), + VariableType::Environment(var_name) => self + .vars_env + .get(var_name.as_str()) + .or_else(|| self.runtime.environment.get(var_name.as_str())) + .cloned(), + VariableType::Envelope(envelope) => self + .envelope + .iter() + .find_map(|(e, v)| if e == envelope { Some(v.clone()) } else { None }), + VariableType::Header(header) => self.eval_header(header), + VariableType::Part(part) => match part { + MessagePart::TextBody(convert) => { + let part = self + .message + .parts + .get(*self.message.text_body.first()? as usize)?; + match &part.body { + PartType::Text(text) => Some(text.as_ref().into()), + PartType::Html(html) if *convert => { + Some(html_to_text(html.as_ref()).into()) + } + _ => None, + } + } + MessagePart::HtmlBody(convert) => { + let part = self + .message + .parts + .get(*self.message.html_body.first()? as usize)?; + match &part.body { + PartType::Html(html) => Some(html.as_ref().into()), + PartType::Text(text) if *convert => { + Some(text_to_html(text.as_ref()).into()) + } + _ => None, + } + } + MessagePart::Contents => match &self.message.parts.get(self.part as usize)?.body { + PartType::Text(text) | PartType::Html(text) => { + Variable::from(text.as_ref()).into() + } + PartType::Binary(bin) | PartType::InlineBinary(bin) => { + Variable::from(String::from_utf8_lossy(bin.as_ref())).into() + } + _ => None, + }, + MessagePart::Raw => { + let part = self.message.parts.get(self.part as usize)?; + self.message + .raw_message() + .get(part.raw_body_offset() as usize..part.raw_end_offset() as usize) + .map(|v| Variable::from(String::from_utf8_lossy(v))) + } + }, + } + } + + pub(crate) fn eval_value(&self, string: &Value) -> Variable { + match string { + Value::Text(text) => Variable::String(text.clone()), + Value::Variable(var) => self.variable(var).unwrap_or_default(), + Value::List(list) => { + let mut data = String::new(); + for item in list { + match item { + Value::Text(string) => { + data.push_str(string); + } + Value::Variable(var) => { + if let Some(value) = self.variable(var) { + data.push_str(&value.to_string()); + } + } + Value::List(_) => { + debug_assert!(false, "This should not have happened: {string:?}"); + } + Value::Number(n) => { + data.push_str(&n.to_string()); + } + Value::Regex(_) => (), + } + } + data.into() + } + Value::Number(n) => Variable::from(*n), + Value::Regex(r) => Variable::String(r.expr.clone().into()), + } + } + + fn eval_header<'z: 'x>(&'z self, header: &HeaderVariable) -> Option { + let mut result = Vec::new(); + let part = self.message.part(self.part)?; + let raw = self.message.raw_message(); + if !header.name.is_empty() { + let mut headers = part + .headers + .iter() + .filter(|h| header.name.contains(&h.name)); + match header.index_hdr.cmp(&0) { + Ordering::Greater => { + if let Some(h) = headers.nth((header.index_hdr - 1) as usize) { + header.eval_part(h, raw, &mut result); + } + } + Ordering::Less => { + if let Some(h) = headers + .rev() + .nth((header.index_hdr.unsigned_abs() - 1) as usize) + { + header.eval_part(h, raw, &mut result); + } + } + Ordering::Equal => { + for h in headers { + header.eval_part(h, raw, &mut result); + } + } + } + } else { + for h in &part.headers { + match &header.part { + HeaderPart::Raw => { + if let Some(var) = raw + .get(h.offset_field as usize..h.offset_end as usize) + .map(sanitize_raw_header) + { + result.push(Variable::from(var)); + } + } + HeaderPart::Text => { + if let HeaderValue::Text(text) = &h.value { + result.push(Variable::from(format!("{}: {}", h.name.as_str(), text))); + } else if let HeaderValue::Text(text) = MessageStream::new( + raw.get(h.offset_start as usize..h.offset_end as usize) + .unwrap_or(b""), + ) + .parse_unstructured() + { + result.push(Variable::from(format!("{}: {}", h.name.as_str(), text))); + } + } + _ => { + header.eval_part(h, raw, &mut result); + } + } + } + } + + match result.len() { + 1 if header.index_hdr != 0 && header.index_part != 0 => result.pop(), + 0 => None, + _ => Some(Variable::Array(result.into())), + } + } + + #[inline(always)] + pub(crate) fn eval_values<'z: 'y, 'y>(&'z self, strings: &'y [Value]) -> Vec { + strings.iter().map(|s| self.eval_value(s)).collect() + } + + #[inline(always)] + pub(crate) fn eval_values_owned(&self, strings: &[Value]) -> Vec { + strings + .iter() + .map(|s| self.eval_value(s).to_string().into_owned()) + .collect() + } +} + +impl<'x> HeaderVariable<'x> { + fn eval_part(&self, header: &'x Header<'x>, raw: &'x [u8], result: &mut Vec) { + let var = match &self.part { + HeaderPart::Text => match &header.value { + HeaderValue::Text(v) if self.include_single_part() => { + Some(Variable::from(v.as_ref())) + } + HeaderValue::TextList(list) => match self.index_part.cmp(&0) { + Ordering::Greater => list + .get((self.index_part - 1) as usize) + .map(|v| Variable::from(v.as_ref())), + Ordering::Less => list + .iter() + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .map(|v| Variable::from(v.as_ref())), + Ordering::Equal => { + for item in list { + result.push(Variable::from(item.as_ref())); + } + return; + } + }, + HeaderValue::ContentType(ct) => if let Some(st) = &ct.c_subtype { + Variable::from(format!("{}/{}", ct.c_type, st)) + } else { + Variable::from(ct.c_type.as_ref()) + } + .into(), + HeaderValue::Address(list) => { + let mut list = list.iter(); + match self.index_part.cmp(&0) { + Ordering::Greater => list + .nth((self.index_part - 1) as usize) + .map(|a| a.to_text()), + Ordering::Less => list + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .map(|a| a.to_text()), + Ordering::Equal => { + for item in list { + result.push(item.to_text()); + } + return; + } + } + } + HeaderValue::DateTime(_) => raw + .get(header.offset_start as usize..header.offset_end as usize) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .map(|s| s.trim()) + .map(Variable::from), + _ => None, + }, + HeaderPart::Address(part) => match &header.value { + HeaderValue::Address(addr) => { + let mut list = addr.iter(); + match self.index_part.cmp(&0) { + Ordering::Greater => list + .nth((self.index_part - 1) as usize) + .and_then(|a| part.eval_strict(a)) + .map(Variable::from), + Ordering::Less => list + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .and_then(|a| part.eval_strict(a)) + .map(Variable::from), + Ordering::Equal => { + for item in list { + result.push( + part.eval_strict(item) + .map(Variable::from) + .unwrap_or_default(), + ); + } + return; + } + } + } + HeaderValue::Text(_) => { + let addr = raw + .get(header.offset_start as usize..header.offset_end as usize) + .and_then(|bytes| match MessageStream::new(bytes).parse_address() { + HeaderValue::Address(addr) => addr.into(), + _ => None, + }); + if let Some(addr) = addr { + let mut list = addr.iter(); + match self.index_part.cmp(&0) { + Ordering::Greater => list + .nth((self.index_part - 1) as usize) + .and_then(|a| part.eval_strict(a)) + .map(|s| Variable::String(s.to_string().into())), + Ordering::Less => list + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .and_then(|a| part.eval_strict(a)) + .map(|s| Variable::String(s.to_string().into())), + Ordering::Equal => { + for item in list { + result.push( + part.eval_strict(item) + .map(|s| Variable::String(s.to_string().into())) + .unwrap_or_default(), + ); + } + return; + } + } + } else { + None + } + } + _ => None, + }, + HeaderPart::Date => { + if let HeaderValue::DateTime(dt) = &header.value { + Variable::from(dt.to_timestamp()).into() + } else { + raw.get(header.offset_start as usize..header.offset_end as usize) + .and_then(|bytes| match MessageStream::new(bytes).parse_date() { + HeaderValue::DateTime(dt) => Variable::from(dt.to_timestamp()).into(), + _ => None, + }) + } + } + HeaderPart::Id => match &header.name { + HeaderName::MessageId | HeaderName::ResentMessageId => match &header.value { + HeaderValue::Text(id) => Variable::from(id.as_ref()).into(), + HeaderValue::TextList(ids) => { + for id in ids { + result.push(Variable::from(id.as_ref())); + } + return; + } + _ => None, + }, + HeaderName::Other(_) => { + match MessageStream::new( + raw.get(header.offset_start as usize..header.offset_end as usize) + .unwrap_or(b""), + ) + .parse_id() + { + HeaderValue::Text(id) => Variable::from(id).into(), + HeaderValue::TextList(ids) => { + for id in ids { + result.push(Variable::from(id)); + } + return; + } + _ => None, + } + } + _ => None, + }, + + HeaderPart::Raw => raw + .get(header.offset_start as usize..header.offset_end as usize) + .map(sanitize_raw_header) + .map(Variable::from), + HeaderPart::RawName => raw + .get(header.offset_field as usize..header.offset_start as usize - 1) + .map(|bytes| std::str::from_utf8(bytes).unwrap_or_default()) + .map(Variable::from), + HeaderPart::Exists => Variable::from(true).into(), + _ => match (&header.value, &self.part) { + (HeaderValue::ContentType(ct), HeaderPart::ContentType(part)) => match part { + ContentTypePart::Type => Variable::from(ct.c_type.as_ref()).into(), + ContentTypePart::Subtype => { + ct.c_subtype.as_ref().map(|s| Variable::from(s.as_ref())) + } + ContentTypePart::Attribute(attr) => ct.attributes.as_ref().and_then(|attrs| { + attrs.iter().find_map(|a| { + if a.name.eq_ignore_ascii_case(attr) { + Some(Variable::from(a.value.as_ref())) + } else { + None + } + }) + }), + }, + (HeaderValue::Received(rcvd), HeaderPart::Received(part)) => part.eval(rcvd), + _ => None, + }, + }; + + result.push(var.unwrap_or_default()); + } + + #[inline(always)] + fn include_single_part(&self) -> bool { + [-1, 0, 1].contains(&self.index_part) + } +} + +impl ReceivedPart { + pub fn eval<'x>(&self, rcvd: &'x Received<'x>) -> Option { + match self { + ReceivedPart::From(from) => rcvd + .from() + .or_else(|| rcvd.helo()) + .and_then(|v| from.to_variable(v)), + ReceivedPart::FromIp => rcvd.from_ip().map(|ip| Variable::from(ip.to_string())), + ReceivedPart::FromIpRev => rcvd.from_iprev().map(Variable::from), + ReceivedPart::By(by) => rcvd.by().and_then(|v: &Host<'_>| by.to_variable(v)), + ReceivedPart::For => rcvd.for_().map(Variable::from), + ReceivedPart::With => rcvd.with().map(|v| Variable::from(v.as_str())), + ReceivedPart::TlsVersion => rcvd.tls_version().map(|v| Variable::from(v.as_str())), + ReceivedPart::TlsCipher => rcvd.tls_cipher().map(Variable::from), + ReceivedPart::Id => rcvd.id().map(Variable::from), + ReceivedPart::Ident => rcvd.ident().map(Variable::from), + ReceivedPart::Via => rcvd.via().map(Variable::from), + ReceivedPart::Date => rcvd.date().map(|d| Variable::from(d.to_timestamp())), + ReceivedPart::DateRaw => rcvd.date().map(|d| Variable::from(d.to_rfc822())), + } + } +} + +trait AddrToText<'x> { + fn to_text<'z: 'x>(&'z self) -> Variable; +} + +impl<'x> AddrToText<'x> for Addr<'x> { + fn to_text<'z: 'x>(&'z self) -> Variable { + if let Some(name) = &self.name { + if let Some(address) = &self.address { + Variable::String(format!("{name} <{address}>").into()) + } else { + Variable::String(name.to_string().into()) + } + } else if let Some(address) = &self.address { + Variable::String(format!("<{address}>").into()) + } else { + Variable::default() + } + } +} + +impl ReceivedHostname { + fn to_variable<'x>(&self, host: &'x Host<'x>) -> Option { + match (self, host) { + (ReceivedHostname::Name, Host::Name(name)) => Variable::from(name.as_ref()).into(), + (ReceivedHostname::Ip, Host::IpAddr(ip)) => Variable::from(ip.to_string()).into(), + (ReceivedHostname::Any, _) => Variable::from(host.to_string()).into(), + _ => None, + } + } +} + +pub(crate) trait IntoString: Sized { + fn into_string(self) -> String; +} + +pub(crate) trait ToString: Sized { + fn to_string(&self) -> String; +} + +impl IntoString for Vec { + fn into_string(self) -> String { + String::from_utf8(self) + .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) + } +} + +fn sanitize_raw_header(bytes: &[u8]) -> String { + let mut result = Vec::with_capacity(bytes.len()); + let mut last_is_space = false; + + for &ch in bytes { + if ch.is_ascii_whitespace() { + last_is_space = true; + } else { + if last_is_space { + result.push(b' '); + last_is_space = false; + } + result.push(ch); + } + } + + result.into_string() +} diff --git a/vendor/sieve-rs/src/runtime/expression.rs b/vendor/sieve-rs/src/runtime/expression.rs new file mode 100644 index 0000000..eef2e6c --- /dev/null +++ b/vendor/sieve-rs/src/runtime/expression.rs @@ -0,0 +1,641 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{cmp::Ordering, fmt::Display}; + +use crate::Event; +use crate::compiler::grammar::expr::parser::ID_EXTERNAL; +use crate::{Context, compiler::Number, runtime::Variable}; + +use crate::compiler::grammar::expr::{BinaryOperator, Constant, Expression, UnaryOperator}; + +impl Context<'_> { + pub(crate) fn eval_expression(&mut self, expr: &[Expression]) -> Result { + let mut exprs = expr.iter().skip(self.expr_pos); + while let Some(expr) = exprs.next() { + self.expr_pos += 1; + match expr { + Expression::Variable(v) => { + self.expr_stack.push(self.variable(v).unwrap_or_default()); + } + Expression::Constant(val) => { + self.expr_stack.push(Variable::from(val)); + } + Expression::UnaryOperator(op) => { + let value = self.expr_stack.pop().unwrap_or_default(); + self.expr_stack.push(match op { + UnaryOperator::Not => value.op_not(), + UnaryOperator::Minus => value.op_minus(), + }); + } + Expression::BinaryOperator(op) => { + let right = self.expr_stack.pop().unwrap_or_default(); + let left = self.expr_stack.pop().unwrap_or_default(); + self.expr_stack.push(match op { + BinaryOperator::Add => left.op_add(right), + BinaryOperator::Subtract => left.op_subtract(right), + BinaryOperator::Multiply => left.op_multiply(right), + BinaryOperator::Divide => left.op_divide(right), + BinaryOperator::And => left.op_and(right), + BinaryOperator::Or => left.op_or(right), + BinaryOperator::Xor => left.op_xor(right), + BinaryOperator::Eq => left.op_eq(right), + BinaryOperator::Ne => left.op_ne(right), + BinaryOperator::Lt => left.op_lt(right), + BinaryOperator::Le => left.op_le(right), + BinaryOperator::Gt => left.op_gt(right), + BinaryOperator::Ge => left.op_ge(right), + }); + } + Expression::Function { id, num_args } => { + let num_args = *num_args as usize; + + if let Some(fnc) = self.runtime.functions.get(*id as usize) { + let mut arguments = vec![Variable::Integer(0); num_args]; + for arg_num in 0..num_args { + arguments[num_args - arg_num - 1] = + self.expr_stack.pop().unwrap_or_default(); + } + self.expr_stack.push((fnc)(self, arguments)); + } else { + let mut arguments = vec![Variable::Integer(0); num_args]; + for arg_num in 0..num_args { + arguments[num_args - arg_num - 1] = + self.expr_stack.pop().unwrap_or_default(); + } + self.pos -= 1; // We need to re-evaluate the function call + return Err(Event::Function { + id: ID_EXTERNAL - *id, + arguments, + }); + } + } + Expression::JmpIf { val, pos } => { + if self.expr_stack.last().is_some_and(|v| v.to_bool()) == *val { + self.expr_pos += *pos as usize; + for _ in 0..*pos { + exprs.next(); + } + } + } + Expression::ArrayAccess => { + let index = self.expr_stack.pop().unwrap_or_default().to_usize(); + let array = self.expr_stack.pop().unwrap_or_default().into_array(); + self.expr_stack + .push(array.get(index).cloned().unwrap_or_default()); + } + Expression::ArrayBuild(num_items) => { + let num_items = *num_items as usize; + let mut items = vec![Variable::Integer(0); num_items]; + for arg_num in 0..num_items { + items[num_items - arg_num - 1] = self.expr_stack.pop().unwrap_or_default(); + } + self.expr_stack.push(Variable::Array(items.into())); + } + } + } + + let result = self.expr_stack.pop().unwrap_or_default(); + self.expr_stack.clear(); + self.expr_pos = 0; + Ok(result) + } +} + +impl Variable { + pub fn op_add(self, other: Variable) -> Variable { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_add(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a + b), + (Variable::Integer(i), Variable::Float(f)) + | (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 + f), + (Variable::Array(a), Variable::Array(b)) => { + Variable::Array(a.iter().chain(b.iter()).cloned().collect::>().into()) + } + (Variable::Array(a), b) => a.iter().cloned().chain([b]).collect::>().into(), + (a, Variable::Array(b)) => [a] + .into_iter() + .chain(b.iter().cloned()) + .collect::>() + .into(), + (Variable::String(a), b) => { + if !a.is_empty() { + Variable::String(format!("{}{}", a, b).into()) + } else { + b + } + } + (a, Variable::String(b)) => { + if !b.is_empty() { + Variable::String(format!("{}{}", a, b).into()) + } else { + a + } + } + } + } + + pub fn op_subtract(self, other: Variable) -> Variable { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_sub(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a - b), + (Variable::Integer(a), Variable::Float(b)) => Variable::Float(a as f64 - b), + (Variable::Float(a), Variable::Integer(b)) => Variable::Float(a - b as f64), + (Variable::Array(a), b) | (b, Variable::Array(a)) => Variable::Array( + a.iter() + .filter(|v| *v != &b) + .cloned() + .collect::>() + .into(), + ), + (a, b) => a.parse_number().op_subtract(b.parse_number()), + } + } + + pub fn op_multiply(self, other: Variable) -> Variable { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_mul(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a * b), + (Variable::Integer(i), Variable::Float(f)) + | (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 * f), + (a, b) => a.parse_number().op_multiply(b.parse_number()), + } + } + + pub fn op_divide(self, other: Variable) -> Variable { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => { + Variable::Float(if b != 0 { a as f64 / b as f64 } else { 0.0 }) + } + (Variable::Float(a), Variable::Float(b)) => { + Variable::Float(if b != 0.0 { a / b } else { 0.0 }) + } + (Variable::Integer(a), Variable::Float(b)) => { + Variable::Float(if b != 0.0 { a as f64 / b } else { 0.0 }) + } + (Variable::Float(a), Variable::Integer(b)) => { + Variable::Float(if b != 0 { a / b as f64 } else { 0.0 }) + } + (a, b) => a.parse_number().op_divide(b.parse_number()), + } + } + + pub fn op_and(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self.to_bool() & other.to_bool())) + } + + pub fn op_or(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self.to_bool() | other.to_bool())) + } + + pub fn op_xor(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self.to_bool() ^ other.to_bool())) + } + + pub fn op_eq(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self == other)) + } + + pub fn op_ne(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self != other)) + } + + pub fn op_lt(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self < other)) + } + + pub fn op_le(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self <= other)) + } + + pub fn op_gt(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self > other)) + } + + pub fn op_ge(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self >= other)) + } + + pub fn op_not(self) -> Variable { + Variable::Integer(i64::from(!self.to_bool())) + } + + pub fn op_minus(self) -> Variable { + match self { + Variable::Integer(n) => Variable::Integer(-n), + Variable::Float(n) => Variable::Float(-n), + _ => self.parse_number().op_minus(), + } + } + + pub fn parse_number(&self) -> Variable { + match self { + Variable::String(s) if !s.is_empty() => { + if let Ok(n) = s.parse::() { + Variable::Integer(n) + } else if let Ok(n) = s.parse::() { + Variable::Float(n) + } else { + Variable::Integer(0) + } + } + Variable::Integer(n) => Variable::Integer(*n), + Variable::Float(n) => Variable::Float(*n), + Variable::Array(l) => Variable::Integer(l.is_empty() as i64), + _ => Variable::Integer(0), + } + } + + pub fn to_bool(&self) -> bool { + match self { + Variable::Float(f) => *f != 0.0, + Variable::Integer(n) => *n != 0, + Variable::String(s) => !s.is_empty(), + Variable::Array(a) => !a.is_empty(), + } + } +} + +impl PartialEq for Variable { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a == b, + (Self::Float(a), Self::Float(b)) => a == b, + (Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => { + *a as f64 == *b + } + (Self::String(a), Self::String(b)) => a == b, + (Self::String(_), Self::Integer(_) | Self::Float(_)) => &self.parse_number() == other, + (Self::Integer(_) | Self::Float(_), Self::String(_)) => self == &other.parse_number(), + (Self::Array(a), Self::Array(b)) => a == b, + _ => false, + } + } +} + +impl Eq for Variable {} + +#[allow(clippy::non_canonical_partial_ord_impl)] +impl PartialOrd for Variable { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a.partial_cmp(b), + (Self::Float(a), Self::Float(b)) => a.partial_cmp(b), + (Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b), + (Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)), + (Self::String(a), Self::String(b)) => a.partial_cmp(b), + (Self::String(_), Self::Integer(_) | Self::Float(_)) => { + self.parse_number().partial_cmp(other) + } + (Self::Integer(_) | Self::Float(_), Self::String(_)) => { + self.partial_cmp(&other.parse_number()) + } + (Self::Array(a), Self::Array(b)) => a.partial_cmp(b), + (Self::Array(_) | Self::String(_), _) => Ordering::Greater.into(), + (_, Self::Array(_)) => Ordering::Less.into(), + } + } +} + +impl Ord for Variable { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Greater) + } +} + +impl Display for Variable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Variable::String(v) => v.fmt(f), + Variable::Integer(v) => v.fmt(f), + Variable::Float(v) => v.fmt(f), + Variable::Array(v) => { + for (i, v) in v.iter().enumerate() { + if i > 0 { + f.write_str("\n")?; + } + v.fmt(f)?; + } + Ok(()) + } + } + } +} + +impl Number { + pub fn is_non_zero(&self) -> bool { + match self { + Number::Integer(n) => *n != 0, + Number::Float(n) => *n != 0.0, + } + } +} + +impl Default for Number { + fn default() -> Self { + Number::Integer(0) + } +} + +impl From for Number { + #[inline(always)] + fn from(b: bool) -> Self { + Number::Integer(i64::from(b)) + } +} + +impl From for Number { + #[inline(always)] + fn from(n: i64) -> Self { + Number::Integer(n) + } +} + +impl From for Number { + #[inline(always)] + fn from(n: f64) -> Self { + Number::Float(n) + } +} + +impl From for Number { + #[inline(always)] + fn from(n: i32) -> Self { + Number::Integer(n as i64) + } +} + +impl<'x> From<&'x Constant> for Variable { + fn from(value: &'x Constant) -> Self { + match value { + Constant::Integer(i) => Variable::Integer(*i), + Constant::Float(f) => Variable::Float(*f), + Constant::String(s) => Variable::String(s.clone()), + } + } +} + +#[cfg(test)] +mod test { + use ahash::{HashMap, HashMapExt}; + + use crate::{ + compiler::{ + VariableType, + grammar::expr::{ + BinaryOperator, Expression, Token, UnaryOperator, parser::ExpressionParser, + tokenizer::Tokenizer, + }, + }, + runtime::Variable, + }; + + use evalexpr::*; + + pub trait EvalExpression { + fn eval(&self, variables: &HashMap) -> Option; + } + + impl EvalExpression for Vec { + fn eval(&self, variables: &HashMap) -> Option { + let mut stack = Vec::with_capacity(self.len()); + let mut exprs = self.iter(); + + while let Some(expr) = exprs.next() { + match expr { + Expression::Variable(VariableType::Global(v)) => { + stack.push(variables.get(v)?.clone()); + } + Expression::Constant(val) => { + stack.push(Variable::from(val)); + } + Expression::UnaryOperator(op) => { + let value = stack.pop()?; + stack.push(match op { + UnaryOperator::Not => value.op_not(), + UnaryOperator::Minus => value.op_minus(), + }); + } + Expression::BinaryOperator(op) => { + let right = stack.pop()?; + let left = stack.pop()?; + stack.push(match op { + BinaryOperator::Add => left.op_add(right), + BinaryOperator::Subtract => left.op_subtract(right), + BinaryOperator::Multiply => left.op_multiply(right), + BinaryOperator::Divide => left.op_divide(right), + BinaryOperator::And => left.op_and(right), + BinaryOperator::Or => left.op_or(right), + BinaryOperator::Xor => left.op_xor(right), + BinaryOperator::Eq => left.op_eq(right), + BinaryOperator::Ne => left.op_ne(right), + BinaryOperator::Lt => left.op_lt(right), + BinaryOperator::Le => left.op_le(right), + BinaryOperator::Gt => left.op_gt(right), + BinaryOperator::Ge => left.op_ge(right), + }); + } + Expression::JmpIf { val, pos } => { + if stack.last()?.to_bool() == *val { + for _ in 0..*pos { + exprs.next(); + } + } + } + _ => unreachable!("Invalid expression"), + } + } + stack.pop() + } + } + + #[test] + fn eval_expression() { + let mut variables = HashMap::from_iter([ + ("A".to_string(), Variable::Integer(0)), + ("B".to_string(), Variable::Integer(0)), + ("C".to_string(), Variable::Integer(0)), + ("D".to_string(), Variable::Integer(0)), + ("E".to_string(), Variable::Integer(0)), + ("F".to_string(), Variable::Integer(0)), + ("G".to_string(), Variable::Integer(0)), + ("H".to_string(), Variable::Integer(0)), + ("I".to_string(), Variable::Integer(0)), + ("J".to_string(), Variable::Integer(0)), + ]); + let num_vars = variables.len(); + + for expr in [ + "A + B", + "A * B", + "A / B", + "A - B", + "-A", + "A == B", + "A != B", + "A > B", + "A < B", + "A >= B", + "A <= B", + "A + B * C - D / E", + "A + B + C - D - E", + "(A + B) * (C - D) / E", + "A - B + C * D / E * F - G", + "A + B * C - D / E", + "(A + B) * (C - D) / E", + "A - B + C / D * E", + "(A + B) / (C - D) + E", + "A * (B + C) - D / E", + "A / (B - C + D) * E", + "(A + B) * C - D / (E + F)", + "A * B - C + D / E", + "A + B - C * D / E", + "(A * B + C) / D - E", + "A - B / C + D * E", + "A + B * (C - D) / E", + "A * B / C + (D - E)", + "(A - B) * C / D + E", + "A * (B / C) - D + E", + "(A + B) / (C + D) * E", + "A - B * C / D + E", + "A + (B - C) * D / E", + "(A + B) * (C / D) - E", + "A - B / (C * D) + E", + "(A + B) > (C - D) && E <= F", + "A * B == C / D || E - F != G + H", + "A / B >= C * D && E + F < G - H", + "(A * B - C) != (D / E + F) && G > H", + "A - B < C && D + E >= F * G", + "(A * B) > C && (D / E) < F || G == H", + "(A + B) <= (C - D) || E > F && G != H", + "A * B != C + D || E - F == G / H", + "A >= B * C && D < E - F || G != H + I", + "(A / B + C) > D && E * F <= G - H", + "A * (B - C) == D && E / F > G + H", + "(A - B + C) != D || E * F >= G && H < I", + "A < B / C && D + E * F == G - H", + "(A + B * C) <= D && E > F / G", + "(A * B - C) > D || E <= F + G && H != I", + "A != B / C && D == E * F - G", + "A <= B + C - D && E / F > G * H", + "(A - B * C) < D || E >= F + G && H != I", + "(A + B) / C == D && E - F < G * H", + "A * B != C && D >= E + F / G || H < I", + "!(A * B != C) && !(D >= E + F / G) || !(H < I)", + "-A - B - (- C - D) - E - (-F)", + ] { + println!("Testing {}", expr); + for (pos, v) in variables.values_mut().enumerate() { + *v = Variable::Integer(pos as i64 + 1); + } + + assert_expr(expr, &variables); + + for (pos, v) in variables.values_mut().enumerate() { + *v = Variable::Integer((num_vars - pos) as i64); + } + + assert_expr(expr, &variables); + } + + for expr in [ + "true && false", + "!true || false", + "true && !false", + "!(true && false)", + "true || true && false", + "!false && (true || false)", + "!(true || !false) && true", + "!(!true && !false)", + "true || false && !true", + "!(true && true) || !false", + "!(!true || !false) && (!false) && !(!true)", + ] { + let pexp = parse_expression(expr.replace("true", "1").replace("false", "0").as_str()); + let result = pexp.eval(&HashMap::new()).unwrap(); + + //println!("{} => {:?}", expr, result); + + match (eval(expr).expect(expr), result) { + (Value::Float(a), Variable::Float(b)) if a == b => (), + (Value::Float(a), Variable::Integer(b)) if a == b as f64 => (), + (Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (), + (a, b) => { + panic!("{} => {:?} != {:?}", expr, a, b) + } + } + } + } + + fn assert_expr(expr: &str, variables: &HashMap) { + let e = parse_expression(expr); + + let result = e.eval(variables).unwrap(); + + let mut str_expr = expr.to_string(); + let mut str_expr_float = expr.to_string(); + for (k, v) in variables { + let v = v.to_string(); + + if v.contains('.') { + str_expr_float = str_expr_float.replace(k, &v); + } else { + str_expr_float = str_expr_float.replace(k, &format!("{}.0", v)); + } + str_expr = str_expr.replace(k, &v); + } + + assert_eq!( + parse_expression(&str_expr) + .eval(&HashMap::new()) + .unwrap() + .to_number() + .to_float(), + result.to_number().to_float() + ); + + assert_eq!( + parse_expression(&str_expr_float) + .eval(&HashMap::new()) + .unwrap() + .to_number() + .to_float(), + result.to_number().to_float() + ); + + //println!("{str_expr} ({e:?}) => {result:?}"); + + match ( + eval(&str_expr_float) + .map(|v| { + // Divisions by zero are converted to 0.0 + if matches!(&v, Value::Float(f) if f64::is_infinite(*f)) { + Value::Float(0.0) + } else { + v + } + }) + .expect(&str_expr), + result, + ) { + (Value::Float(a), Variable::Float(b)) if a == b => (), + (Value::Float(a), Variable::Integer(b)) if a == b as f64 => (), + (Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (), + (a, b) => { + panic!("{} => {:?} != {:?}", str_expr, a, b) + } + } + } + + fn parse_expression(expr: &str) -> Vec { + ExpressionParser::from_tokenizer(Tokenizer::new(expr, |var_name: &str, _: bool| { + Ok::<_, String>(Token::Variable(VariableType::Global(var_name.to_string()))) + })) + .parse() + .unwrap() + .output + } +} diff --git a/vendor/sieve-rs/src/runtime/mod.rs b/vendor/sieve-rs/src/runtime/mod.rs new file mode 100644 index 0000000..9e19150 --- /dev/null +++ b/vendor/sieve-rs/src/runtime/mod.rs @@ -0,0 +1,803 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. + */ + +pub mod actions; +pub mod context; +pub mod eval; +pub mod expression; +pub mod tests; +pub mod variables; + +use ahash::{AHashMap, AHashSet}; +use mail_parser::HeaderName; +#[cfg(not(test))] +use mail_parser::{Encoding, Message, MessageParser, MessagePart, PartType}; +use std::{borrow::Cow, fmt::Display, hash::Hash, ops::Deref, sync::Arc}; + +#[cfg(not(test))] +use crate::Context; + +use crate::{ + ExternalId, Function, FunctionMap, Input, Metadata, Runtime, Script, Sieve, + compiler::{ + Number, + grammar::{Capability, Invalid, expr::parser::ID_EXTERNAL}, + }, +}; + +use self::eval::ToString; + +#[derive(Debug, Clone)] +#[cfg_attr( + any(test, feature = "serde"), + derive(serde::Serialize, serde::Deserialize) +)] +pub enum Variable { + String(Arc), + Integer(i64), + Float(f64), + Array(Arc>), +} + +#[derive(Debug)] +pub enum RuntimeError { + TooManyIncludes, + InvalidInstruction(Invalid), + ScriptErrorMessage(String), + CapabilityNotAllowed(Capability), + CapabilityNotSupported(String), + CPULimitReached, +} + +impl Default for Variable { + fn default() -> Self { + Variable::String(Arc::new(String::new())) + } +} + +impl Variable { + pub fn to_string(&self) -> Cow<'_, str> { + match self { + Variable::String(s) => Cow::Borrowed(s.as_str()), + Variable::Integer(n) => Cow::Owned(n.to_string()), + Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::Array(l) => Cow::Owned(l.to_string()), + } + } + + pub fn to_number(&self) -> Number { + self.to_number_checked() + .unwrap_or(Number::Float(f64::INFINITY)) + } + + pub fn to_number_checked(&self) -> Option { + let s = match self { + Variable::Integer(n) => return Number::Integer(*n).into(), + Variable::Float(n) => return Number::Float(*n).into(), + Variable::String(s) if !s.is_empty() => s.as_str(), + _ => return None, + }; + + if !s.contains('.') { + s.parse::().map(Number::Integer).ok() + } else { + s.parse::().map(Number::Float).ok() + } + } + + pub fn to_integer(&self) -> i64 { + match self { + Variable::Integer(n) => *n, + Variable::Float(n) => *n as i64, + Variable::String(s) if !s.is_empty() => s.parse::().unwrap_or(0), + _ => 0, + } + } + + pub fn to_usize(&self) -> usize { + match self { + Variable::Integer(n) => *n as usize, + Variable::Float(n) => *n as usize, + Variable::String(s) if !s.is_empty() => s.parse::().unwrap_or(0), + _ => 0, + } + } + + pub fn len(&self) -> usize { + match self { + Variable::String(s) => s.len(), + Variable::Integer(_) | Variable::Float(_) => 2, + Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + Variable::String(s) => s.is_empty(), + _ => false, + } + } + + pub fn as_array(&self) -> Option<&[Variable]> { + match self { + Variable::Array(l) => Some(l), + _ => None, + } + } + + pub fn into_array(self) -> Arc> { + match self { + Variable::Array(l) => l, + v if !v.is_empty() => vec![v].into(), + _ => vec![].into(), + } + } + + pub fn to_array(&self) -> Arc> { + match self { + Variable::Array(l) => l.clone(), + v if !v.is_empty() => vec![v.clone()].into(), + _ => vec![].into(), + } + } + + pub fn into_string_array(self) -> Vec { + match self { + Variable::Array(l) => l.iter().map(|i| i.to_string().into_owned()).collect(), + v if !v.is_empty() => vec![v.to_string().into_owned()], + _ => vec![], + } + } + + pub fn to_string_array(&self) -> Vec> { + match self { + Variable::Array(l) => l.iter().map(|i| i.to_string()).collect(), + v if !v.is_empty() => vec![v.to_string()], + _ => vec![], + } + } +} + +impl From for Variable { + fn from(s: String) -> Self { + Variable::String(s.into()) + } +} + +impl<'x> From<&'x String> for Variable { + fn from(s: &'x String) -> Self { + Variable::String(s.as_str().to_string().into()) + } +} + +impl<'x> From<&'x str> for Variable { + fn from(s: &'x str) -> Self { + Variable::String(s.to_string().into()) + } +} + +impl<'x> From> for Variable { + fn from(s: Cow<'x, str>) -> Self { + match s { + Cow::Borrowed(s) => Variable::String(s.to_string().into()), + Cow::Owned(s) => Variable::String(s.into()), + } + } +} + +impl From> for Variable { + fn from(l: Vec) -> Self { + Variable::Array(l.into()) + } +} + +impl From for Variable { + fn from(n: Number) -> Self { + match n { + Number::Integer(n) => Variable::Integer(n), + Number::Float(n) => Variable::Float(n), + } + } +} + +impl From for Variable { + fn from(n: usize) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable { + fn from(n: i64) -> Self { + Variable::Integer(n) + } +} + +impl From for Variable { + fn from(n: u64) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable { + fn from(n: f64) -> Self { + Variable::Float(n) + } +} + +impl From for Variable { + fn from(n: i32) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable { + fn from(n: u32) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable { + fn from(b: bool) -> Self { + Variable::Integer(i64::from(b)) + } +} + +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a == b, + (Self::Float(a), Self::Float(b)) => a == b, + (Self::Integer(a), Self::Float(b)) => (*a as f64) == *b, + (Self::Float(a), Self::Integer(b)) => *a == (*b as f64), + } + } +} + +impl Eq for Number {} + +impl PartialOrd for Number { + fn partial_cmp(&self, other: &Self) -> Option { + let (a, b) = match (self, other) { + (Number::Integer(a), Number::Integer(b)) => return a.partial_cmp(b), + (Number::Float(a), Number::Float(b)) => (*a, *b), + (Number::Integer(a), Number::Float(b)) => (*a as f64, *b), + (Number::Float(a), Number::Integer(b)) => (*a, *b as f64), + }; + a.partial_cmp(&b) + } +} + +impl self::eval::ToString for Vec { + fn to_string(&self) -> String { + let mut result = String::with_capacity(self.len() * 10); + for item in self { + if !result.is_empty() { + result.push_str("\r\n"); + } + match item { + Variable::String(v) => result.push_str(v), + Variable::Integer(v) => result.push_str(&v.to_string()), + Variable::Float(v) => result.push_str(&v.to_string()), + Variable::Array(_) => {} + } + } + result + } +} + +impl Hash for Variable { + fn hash(&self, state: &mut H) { + match self { + Variable::String(s) => s.hash(state), + Variable::Integer(n) => n.hash(state), + Variable::Float(n) => n.to_bits().hash(state), + Variable::Array(l) => l.hash(state), + } + } +} + +#[cfg(not(test))] +impl Runtime { + pub fn filter<'z: 'x, 'x>(&'z self, raw_message: &'x [u8]) -> Context<'x> { + Context::new( + self, + MessageParser::new() + .parse(raw_message) + .unwrap_or_else(|| Message { + parts: vec![MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text("".into()), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }], + raw_message: b""[..].into(), + ..Default::default() + }), + ) + } + + pub fn filter_parsed<'z: 'x, 'x>(&'z self, message: Message<'x>) -> Context<'x> { + Context::new(self, message) + } +} + +impl Default for Runtime { + fn default() -> Self { + Self::new() + } +} + +impl Runtime { + pub fn new() -> Self { + #[allow(unused_mut)] + let mut allowed_capabilities = AHashSet::from_iter(Capability::all().iter().cloned()); + + #[cfg(test)] + allowed_capabilities.insert(Capability::Other("vnd.inbuxa.testsuite".to_string())); + + Runtime { + allowed_capabilities, + environment: AHashMap::from_iter([ + ("name".into(), "inbuxa Sieve".into()), + ("version".into(), env!("CARGO_PKG_VERSION").into()), + ]), + metadata: Vec::new(), + include_scripts: AHashMap::new(), + max_nested_includes: 3, + cpu_limit: 5000, + max_variable_size: 4096, + max_redirects: 1, + max_received_headers: 10, + protected_headers: vec![ + HeaderName::Other("Original-Subject".into()), + HeaderName::Other("Original-From".into()), + ], + valid_notification_uris: AHashSet::new(), + valid_ext_lists: AHashSet::new(), + vacation_use_orig_rcpt: false, + vacation_default_subject: "Automated reply".into(), + vacation_subject_prefix: "Auto: ".into(), + max_header_size: 1024, + max_out_messages: 3, + default_vacation_expiry: 30 * 86400, + default_duplicate_expiry: 7 * 86400, + local_hostname: "localhost".into(), + functions: Vec::new(), + } + } + + pub fn set_cpu_limit(&mut self, size: usize) { + self.cpu_limit = size; + } + + pub fn with_cpu_limit(mut self, size: usize) -> Self { + self.cpu_limit = size; + self + } + + pub fn set_max_nested_includes(&mut self, size: usize) { + self.max_nested_includes = size; + } + + pub fn with_max_nested_includes(mut self, size: usize) -> Self { + self.max_nested_includes = size; + self + } + + pub fn set_max_redirects(&mut self, size: usize) { + self.max_redirects = size; + } + + pub fn with_max_redirects(mut self, size: usize) -> Self { + self.max_redirects = size; + self + } + + pub fn set_max_out_messages(&mut self, size: usize) { + self.max_out_messages = size; + } + + pub fn with_max_out_messages(mut self, size: usize) -> Self { + self.max_out_messages = size; + self + } + + pub fn set_max_received_headers(&mut self, size: usize) { + self.max_received_headers = size; + } + + pub fn with_max_received_headers(mut self, size: usize) -> Self { + self.max_received_headers = size; + self + } + + pub fn set_max_variable_size(&mut self, size: usize) { + self.max_variable_size = size; + } + + pub fn with_max_variable_size(mut self, size: usize) -> Self { + self.max_variable_size = size; + self + } + + pub fn set_max_header_size(&mut self, size: usize) { + self.max_header_size = size; + } + + pub fn with_max_header_size(mut self, size: usize) -> Self { + self.max_header_size = size; + self + } + + pub fn set_default_vacation_expiry(&mut self, expiry: u64) { + self.default_vacation_expiry = expiry; + } + + pub fn with_default_vacation_expiry(mut self, expiry: u64) -> Self { + self.default_vacation_expiry = expiry; + self + } + + pub fn set_default_duplicate_expiry(&mut self, expiry: u64) { + self.default_duplicate_expiry = expiry; + } + + pub fn with_default_duplicate_expiry(mut self, expiry: u64) -> Self { + self.default_duplicate_expiry = expiry; + self + } + + pub fn set_capability(&mut self, capability: impl Into) { + self.allowed_capabilities.insert(capability.into()); + } + + pub fn with_capability(mut self, capability: impl Into) -> Self { + self.set_capability(capability); + self + } + + pub fn unset_capability(&mut self, capability: impl Into) { + self.allowed_capabilities.remove(&capability.into()); + } + + pub fn without_capability(mut self, capability: impl Into) -> Self { + self.unset_capability(capability); + self + } + + pub fn without_capabilities( + mut self, + capabilities: impl IntoIterator>, + ) -> Self { + for capability in capabilities { + self.allowed_capabilities.remove(&capability.into()); + } + self + } + + pub fn set_protected_header(&mut self, header_name: impl Into>) { + if let Some(header_name) = HeaderName::parse(header_name) { + self.protected_headers.push(header_name); + } + } + + pub fn with_protected_header(mut self, header_name: impl Into>) -> Self { + self.set_protected_header(header_name); + self + } + + pub fn with_protected_headers( + mut self, + header_names: impl IntoIterator>>, + ) -> Self { + self.protected_headers = header_names + .into_iter() + .filter_map(HeaderName::parse) + .collect(); + self + } + + pub fn set_env_variable( + &mut self, + name: impl Into>, + value: impl Into, + ) { + self.environment.insert(name.into(), value.into()); + } + + pub fn with_env_variable( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_env_variable(name.into(), value.into()); + self + } + + pub fn set_medatata( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.metadata.push((name.into(), value.into())); + } + + pub fn with_metadata( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_medatata(name, value); + self + } + + pub fn set_valid_notification_uri(&mut self, uri: impl Into>) { + self.valid_notification_uris.insert(uri.into()); + } + + pub fn with_valid_notification_uri(mut self, uri: impl Into>) -> Self { + self.valid_notification_uris.insert(uri.into()); + self + } + + pub fn with_valid_notification_uris( + mut self, + uris: impl IntoIterator>>, + ) -> Self { + self.valid_notification_uris = uris.into_iter().map(Into::into).collect(); + self + } + + pub fn set_valid_ext_list(&mut self, name: impl Into>) { + self.valid_ext_lists.insert(name.into()); + } + + pub fn with_valid_ext_list(mut self, name: impl Into>) -> Self { + self.set_valid_ext_list(name); + self + } + + pub fn set_vacation_use_orig_rcpt(&mut self, value: bool) { + self.vacation_use_orig_rcpt = value; + } + + pub fn with_valid_ext_lists( + mut self, + lists: impl IntoIterator>>, + ) -> Self { + self.valid_ext_lists = lists.into_iter().map(Into::into).collect(); + self + } + + pub fn with_vacation_use_orig_rcpt(mut self, value: bool) -> Self { + self.set_vacation_use_orig_rcpt(value); + self + } + + pub fn set_vacation_default_subject(&mut self, value: impl Into>) { + self.vacation_default_subject = value.into(); + } + + pub fn with_vacation_default_subject(mut self, value: impl Into>) -> Self { + self.set_vacation_default_subject(value); + self + } + + pub fn set_vacation_subject_prefix(&mut self, value: impl Into>) { + self.vacation_subject_prefix = value.into(); + } + + pub fn with_vacation_subject_prefix(mut self, value: impl Into>) -> Self { + self.set_vacation_subject_prefix(value); + self + } + + pub fn set_local_hostname(&mut self, value: impl Into>) { + self.local_hostname = value.into(); + } + + pub fn with_local_hostname(mut self, value: impl Into>) -> Self { + self.set_local_hostname(value); + self + } + + pub fn with_functions(mut self, fnc_map: &mut FunctionMap) -> Self { + self.functions = std::mem::take(&mut fnc_map.functions); + self + } + + pub fn set_functions(&mut self, fnc_map: &mut FunctionMap) { + self.functions = std::mem::take(&mut fnc_map.functions); + } +} + +impl FunctionMap { + pub fn new() -> Self { + FunctionMap { + map: Default::default(), + functions: Default::default(), + } + } + + pub fn with_function(self, name: impl Into, fnc: Function) -> Self { + self.with_function_args(name, fnc, 1) + } + + pub fn with_function_no_args(self, name: impl Into, fnc: Function) -> Self { + self.with_function_args(name, fnc, 0) + } + + pub fn with_function_args( + mut self, + name: impl Into, + fnc: Function, + num_args: u32, + ) -> Self { + self.map + .insert(name.into(), (self.functions.len() as u32, num_args)); + self.functions.push(fnc); + self + } + + pub fn with_external_function( + mut self, + name: impl Into, + id: ExternalId, + num_args: u32, + ) -> Self { + self.set_external_function(name, id, num_args); + self + } + + pub fn set_external_function( + &mut self, + name: impl Into, + id: ExternalId, + num_args: u32, + ) { + self.map.insert(name.into(), (ID_EXTERNAL - id, num_args)); + } +} + +impl Input { + pub fn script(name: impl Into