Files
inbuxa-server/vendor/sieve-rs/src/runtime/mod.rs
T
jcoffey-dev cc6f1eb298
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m53s
Rename the identifiers that carried the upstream name
Everything clients, users and operators meet now carries the fork's name,
with no aliases (SPEC.md §2.4, changed here from "protocol identifiers
stay"):

- JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside
  the fork's own urn:inbuxa:jmap.
- WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once.
- Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these
  into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in;
  a unit test fails if Cargo.lock ever moves past the vendored copy. The
  trusted runtime now names itself too, rather than answering sieve-rs's
  default.
- The web interface's OAuth client is inbuxa-webui. On every start the old
  stalwart-webui client is removed and any application naming it is moved
  over.
- The spam filter's blobs are INBUXA_SPAM_*; every start moves any left
  under the old keys, so a trained model survives.
- SQL stores and log files default to inbuxa, in the code and in the
  schema served to the admin (checksum regenerated).
- Settings are INBUXA_* only. A STALWART_* variable that's set where its
  INBUXA_* one isn't stops the server at startup, naming it.
- The version-upgrade messages link docs.inbuxa.org's migration page, and
  the OpenAPI description, smtp crate metadata and web-push test fixtures
  lose the name.

Kept on purpose, allowlisted with reasons: the OAuth key-derivation
contexts (renaming them would end every session and invalidate every
sealed client id) and the hashed application prefix.

Also fixes a latent start-up failure: ensure_client updated an existing
first-party client with a revision of 0, which the registry's assertion
never matches, so adding a redirect URI or changing the webmail secret
failed start-up. And the principal session test now expects
legacyProtocols (C-1, added 2026-09-21), which it had missed.

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
2026-09-22 19:33:02 -07:00

804 lines
21 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* 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<String>),
Integer(i64),
Float(f64),
Array(Arc<Vec<Variable>>),
}
#[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<Number> {
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::<i64>().map(Number::Integer).ok()
} else {
s.parse::<f64>().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::<i64>().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::<usize>().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<Vec<Variable>> {
match self {
Variable::Array(l) => l,
v if !v.is_empty() => vec![v].into(),
_ => vec![].into(),
}
}
pub fn to_array(&self) -> Arc<Vec<Variable>> {
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<String> {
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<Cow<'_, str>> {
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<String> 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<Cow<'x, str>> 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<Vec<Variable>> for Variable {
fn from(l: Vec<Variable>) -> Self {
Variable::Array(l.into())
}
}
impl From<Number> for Variable {
fn from(n: Number) -> Self {
match n {
Number::Integer(n) => Variable::Integer(n),
Number::Float(n) => Variable::Float(n),
}
}
}
impl From<usize> for Variable {
fn from(n: usize) -> Self {
Variable::Integer(n as i64)
}
}
impl From<i64> for Variable {
fn from(n: i64) -> Self {
Variable::Integer(n)
}
}
impl From<u64> for Variable {
fn from(n: u64) -> Self {
Variable::Integer(n as i64)
}
}
impl From<f64> for Variable {
fn from(n: f64) -> Self {
Variable::Float(n)
}
}
impl From<i32> for Variable {
fn from(n: i32) -> Self {
Variable::Integer(n as i64)
}
}
impl From<u32> for Variable {
fn from(n: u32) -> Self {
Variable::Integer(n as i64)
}
}
impl From<bool> 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<std::cmp::Ordering> {
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<Variable> {
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<H: std::hash::Hasher>(&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<Capability>) {
self.allowed_capabilities.insert(capability.into());
}
pub fn with_capability(mut self, capability: impl Into<Capability>) -> Self {
self.set_capability(capability);
self
}
pub fn unset_capability(&mut self, capability: impl Into<Capability>) {
self.allowed_capabilities.remove(&capability.into());
}
pub fn without_capability(mut self, capability: impl Into<Capability>) -> Self {
self.unset_capability(capability);
self
}
pub fn without_capabilities(
mut self,
capabilities: impl IntoIterator<Item = impl Into<Capability>>,
) -> Self {
for capability in capabilities {
self.allowed_capabilities.remove(&capability.into());
}
self
}
pub fn set_protected_header(&mut self, header_name: impl Into<Cow<'static, str>>) {
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<Cow<'static, str>>) -> Self {
self.set_protected_header(header_name);
self
}
pub fn with_protected_headers(
mut self,
header_names: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.protected_headers = header_names
.into_iter()
.filter_map(HeaderName::parse)
.collect();
self
}
pub fn set_env_variable(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) {
self.environment.insert(name.into(), value.into());
}
pub fn with_env_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_env_variable(name.into(), value.into());
self
}
pub fn set_medatata(
&mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'static, str>>,
) {
self.metadata.push((name.into(), value.into()));
}
pub fn with_metadata(
mut self,
name: impl Into<Metadata<String>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.set_medatata(name, value);
self
}
pub fn set_valid_notification_uri(&mut self, uri: impl Into<Cow<'static, str>>) {
self.valid_notification_uris.insert(uri.into());
}
pub fn with_valid_notification_uri(mut self, uri: impl Into<Cow<'static, str>>) -> Self {
self.valid_notification_uris.insert(uri.into());
self
}
pub fn with_valid_notification_uris(
mut self,
uris: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Self {
self.valid_notification_uris = uris.into_iter().map(Into::into).collect();
self
}
pub fn set_valid_ext_list(&mut self, name: impl Into<Cow<'static, str>>) {
self.valid_ext_lists.insert(name.into());
}
pub fn with_valid_ext_list(mut self, name: impl Into<Cow<'static, str>>) -> 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<Item = impl Into<Cow<'static, str>>>,
) -> 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<Cow<'static, str>>) {
self.vacation_default_subject = value.into();
}
pub fn with_vacation_default_subject(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_vacation_default_subject(value);
self
}
pub fn set_vacation_subject_prefix(&mut self, value: impl Into<Cow<'static, str>>) {
self.vacation_subject_prefix = value.into();
}
pub fn with_vacation_subject_prefix(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.set_vacation_subject_prefix(value);
self
}
pub fn set_local_hostname(&mut self, value: impl Into<Cow<'static, str>>) {
self.local_hostname = value.into();
}
pub fn with_local_hostname(mut self, value: impl Into<Cow<'static, str>>) -> 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<String>, fnc: Function) -> Self {
self.with_function_args(name, fnc, 1)
}
pub fn with_function_no_args(self, name: impl Into<String>, fnc: Function) -> Self {
self.with_function_args(name, fnc, 0)
}
pub fn with_function_args(
mut self,
name: impl Into<String>,
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<String>,
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<String>,
id: ExternalId,
num_args: u32,
) {
self.map.insert(name.into(), (ID_EXTERNAL - id, num_args));
}
}
impl Input {
pub fn script(name: impl Into<Script>, script: impl Into<Arc<Sieve>>) -> Self {
Input::Script {
name: name.into(),
script: script.into(),
}
}
pub fn success() -> Self {
Input::True
}
pub fn fail() -> Self {
Input::False
}
pub fn result(result: Variable) -> Self {
Input::FncResult(result)
}
}
impl From<bool> for Input {
fn from(value: bool) -> Self {
if value { Input::True } else { Input::False }
}
}
impl From<Variable> for Input {
fn from(value: Variable) -> Self {
Input::FncResult(value)
}
}
impl Deref for Script {
type Target = String;
fn deref(&self) -> &Self::Target {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl AsRef<str> for Script {
fn as_ref(&self) -> &str {
match self {
Script::Personal(name) | Script::Global(name) => name.as_str(),
}
}
}
impl AsRef<String> for Script {
fn as_ref(&self) -> &String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl Script {
pub fn into_string(self) -> String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
pub fn as_str(&self) -> &String {
match self {
Script::Personal(name) | Script::Global(name) => name,
}
}
}
impl Display for Script {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<String> for Script {
fn from(name: String) -> Self {
Script::Personal(name)
}
}
impl From<&str> for Script {
fn from(name: &str) -> Self {
Script::Personal(name.to_string())
}
}
impl<T> Metadata<T> {
pub fn server(annotation: impl Into<T>) -> Self {
Metadata::Server {
annotation: annotation.into(),
}
}
pub fn mailbox(name: impl Into<T>, annotation: impl Into<T>) -> Self {
Metadata::Mailbox {
name: name.into(),
annotation: annotation.into(),
}
}
}
impl From<String> for Metadata<String> {
fn from(annotation: String) -> Self {
Metadata::Server { annotation }
}
}
impl From<&'_ str> for Metadata<String> {
fn from(annotation: &'_ str) -> Self {
Metadata::Server {
annotation: annotation.to_string(),
}
}
}
impl From<(String, String)> for Metadata<String> {
fn from((name, annotation): (String, String)) -> Self {
Metadata::Mailbox { name, annotation }
}
}
impl From<(&'_ str, &'_ str)> for Metadata<String> {
fn from((name, annotation): (&'_ str, &'_ str)) -> Self {
Metadata::Mailbox {
name: name.to_string(),
annotation: annotation.to_string(),
}
}
}