Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
@@ -0,0 +1,159 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::collections::{HashMap, HashSet};
use sieve::{Context, runtime::Variable};
pub fn fn_count<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::Array(a) => a.len(),
v => {
if !v.is_empty() {
1
} else {
0
}
}
}
.into()
}
pub fn fn_sort<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let is_asc = v[1].to_bool();
let mut arr = (*v[0].to_array()).clone();
if is_asc {
arr.sort_unstable();
} else {
arr.sort_unstable_by(|a, b| b.cmp(a));
}
arr.into()
}
pub fn fn_dedup<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let arr = v[0].to_array();
let mut result = Vec::with_capacity(arr.len());
for item in arr.iter() {
if !result.contains(item) {
result.push(item.clone());
}
}
result.into()
}
pub fn fn_cosine_similarity<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let mut word_freq: HashMap<Variable, [u32; 2]> = HashMap::new();
for (idx, var) in v.into_iter().enumerate() {
match var {
Variable::Array(l) => {
for item in l.iter() {
word_freq.entry(item.clone()).or_insert([0, 0])[idx] += 1;
}
}
_ => {
for char in var.to_string().chars() {
word_freq.entry(char.to_string().into()).or_insert([0, 0])[idx] += 1;
}
}
}
}
let mut dot_product = 0;
let mut magnitude_a = 0;
let mut magnitude_b = 0;
for count in word_freq.values() {
dot_product += count[0] * count[1];
magnitude_a += count[0] * count[0];
magnitude_b += count[1] * count[1];
}
if magnitude_a != 0 && magnitude_b != 0 {
dot_product as f64 / (magnitude_a as f64).sqrt() / (magnitude_b as f64).sqrt()
} else {
0.0
}
.into()
}
pub fn cosine_similarity(a: &[&str], b: &[&str]) -> f64 {
let mut word_freq: HashMap<&str, [u32; 2]> = HashMap::new();
for (idx, items) in [a, b].into_iter().enumerate() {
for item in items {
word_freq.entry(item).or_insert([0, 0])[idx] += 1;
}
}
let mut dot_product = 0;
let mut magnitude_a = 0;
let mut magnitude_b = 0;
for count in word_freq.values() {
dot_product += count[0] * count[1];
magnitude_a += count[0] * count[0];
magnitude_b += count[1] * count[1];
}
if magnitude_a != 0 && magnitude_b != 0 {
dot_product as f64 / (magnitude_a as f64).sqrt() / (magnitude_b as f64).sqrt()
} else {
0.0
}
}
pub fn fn_jaccard_similarity<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let mut word_freq = [HashSet::new(), HashSet::new()];
for (idx, var) in v.into_iter().enumerate() {
match var {
Variable::Array(l) => {
for item in l.iter() {
word_freq[idx].insert(item.clone());
}
}
_ => {
for char in var.to_string().chars() {
word_freq[idx].insert(char.to_string().into());
}
}
}
}
let intersection_size = word_freq[0].intersection(&word_freq[1]).count();
let union_size = word_freq[0].union(&word_freq[1]).count();
if union_size != 0 {
intersection_size as f64 / union_size as f64
} else {
0.0
}
.into()
}
pub fn fn_is_intersect<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match (&v[0], &v[1]) {
(Variable::Array(a), Variable::Array(b)) => a.iter().any(|x| b.contains(x)),
(Variable::Array(a), item) | (item, Variable::Array(a)) => a.contains(item),
_ => false,
}
.into()
}
pub fn fn_winnow<'x>(_: &'x Context<'x>, mut v: Vec<Variable>) -> Variable {
match v.remove(0) {
Variable::Array(a) => a
.iter()
.filter(|i| !i.is_empty())
.cloned()
.collect::<Vec<_>>()
.into(),
v => v,
}
}
@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::{Context, runtime::Variable};
use super::ApplyString;
pub fn fn_is_email<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let mut last_ch = 0;
let mut in_quote = false;
let mut at_count = 0;
let mut dot_count = 0;
let mut lp_len = 0;
let mut value = 0;
for ch in v[0].to_string().bytes() {
match ch {
b'0'..=b'9'
| b'a'..=b'z'
| b'A'..=b'Z'
| b'!'
| b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'/'
| b'='
| b'?'
| b'^'
| b'_'
| b'`'
| b'{'
| b'|'
| b'}'
| b'~'
| 0x7f..=u8::MAX => {
value += 1;
}
b'.' if !in_quote => {
if last_ch != b'.' && last_ch != b'@' && value != 0 {
value += 1;
if at_count == 1 {
dot_count += 1;
}
} else {
return false.into();
}
}
b'@' if !in_quote => {
at_count += 1;
lp_len = value;
value = 0;
}
b'>' | b':' | b',' | b' ' if in_quote => {
value += 1;
}
b'\"' if !in_quote || last_ch != b'\\' => {
in_quote = !in_quote;
}
b'\\' if in_quote && last_ch != b'\\' => (),
_ => {
if !in_quote {
return false.into();
}
}
}
last_ch = ch;
}
(at_count == 1 && dot_count > 0 && lp_len > 0 && value > 0).into()
}
pub fn fn_email_part<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| {
s.rsplit_once('@')
.map(|(u, d)| match v[1].to_string().as_ref() {
"local" => Variable::from(u.trim()),
"domain" => Variable::from(d.trim()),
_ => Variable::default(),
})
.unwrap_or_default()
})
}
@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::{HeaderName, HeaderValue, MimeHeaders, parsers::fields::thread::thread_name};
use sieve::{Context, compiler::ReceivedPart, runtime::Variable};
use super::ApplyString;
pub fn fn_received_part<'x>(ctx: &'x Context<'x>, v: Vec<Variable>) -> Variable {
if let (Ok(part), Some(HeaderValue::Received(rcvd))) = (
ReceivedPart::try_from(v[1].to_string().as_ref()),
ctx.message()
.part(ctx.part())
.and_then(|p| {
p.headers
.iter()
.filter(|h| h.name == HeaderName::Received)
.nth((v[0].to_integer() as usize).saturating_sub(1))
})
.map(|h| &h.value),
) {
part.eval(rcvd).unwrap_or_default()
} else {
Variable::default()
}
}
pub fn fn_is_encoding_problem<'x>(ctx: &'x Context<'x>, _: Vec<Variable>) -> Variable {
ctx.message()
.part(ctx.part())
.map(|p| p.is_encoding_problem)
.unwrap_or_default()
.into()
}
pub fn fn_is_attachment<'x>(ctx: &'x Context<'x>, _: Vec<Variable>) -> Variable {
ctx.message().attachments.contains(&ctx.part()).into()
}
pub fn fn_is_body<'x>(ctx: &'x Context<'x>, _: Vec<Variable>) -> Variable {
(ctx.message().text_body.contains(&ctx.part()) || ctx.message().html_body.contains(&ctx.part()))
.into()
}
pub fn fn_attachment_name<'x>(ctx: &'x Context<'x>, _: Vec<Variable>) -> Variable {
ctx.message()
.part(ctx.part())
.and_then(|p| p.attachment_name())
.unwrap_or_default()
.into()
}
pub fn fn_mime_part_len<'x>(ctx: &'x Context<'x>, _: Vec<Variable>) -> Variable {
ctx.message()
.part(ctx.part())
.map(|p| p.len())
.unwrap_or_default()
.into()
}
pub fn fn_thread_name<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| thread_name(s).into())
}
pub fn fn_is_header_utf8_valid<'x>(ctx: &'x Context<'x>, v: Vec<Variable>) -> Variable {
ctx.message()
.part(ctx.part())
.map(|p| {
let raw = ctx.message().raw_message();
let mut is_valid = true;
if let Some(header_name) = HeaderName::parse(v[0].to_string().as_ref()) {
for header in &p.headers {
if header.name == header_name
&& raw
.get(header.offset_start() as usize..header.offset_end() as usize)
.and_then(|raw| std::str::from_utf8(raw).ok())
.is_none()
{
is_valid = false;
break;
}
}
} else {
is_valid = raw
.get(p.raw_header_offset() as usize..p.raw_body_offset() as usize)
.and_then(|raw| std::str::from_utf8(raw).ok())
.is_some();
}
Variable::from(is_valid)
})
.unwrap_or(Variable::Integer(1))
}
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::{Context, runtime::Variable};
pub fn fn_img_metadata<'x>(ctx: &'x Context<'x>, v: Vec<Variable>) -> Variable {
ctx.message()
.part(ctx.part())
.map(|p| p.contents())
.and_then(|bytes| {
let arg = v[1].to_string();
match arg.as_ref() {
"type" => imagesize::image_type(bytes).ok().map(|t| {
Variable::from(match t {
imagesize::ImageType::Aseprite => "aseprite",
imagesize::ImageType::Bmp => "bmp",
imagesize::ImageType::Dds(_) => "dds",
imagesize::ImageType::Exr => "exr",
imagesize::ImageType::Farbfeld => "farbfeld",
imagesize::ImageType::Gif => "gif",
imagesize::ImageType::Hdr => "hdr",
imagesize::ImageType::Heif(_) => "heif",
imagesize::ImageType::Ico => "ico",
imagesize::ImageType::Jpeg => "jpeg",
imagesize::ImageType::Jxl => "jxl",
imagesize::ImageType::Ktx2 => "ktx2",
imagesize::ImageType::Png => "png",
imagesize::ImageType::Pnm => "pnm",
imagesize::ImageType::Psd => "psd",
imagesize::ImageType::Qoi => "qoi",
imagesize::ImageType::Tga => "tga",
imagesize::ImageType::Tiff => "tiff",
imagesize::ImageType::Vtf => "vtf",
imagesize::ImageType::Webp => "webp",
imagesize::ImageType::Ilbm => "ilbm",
_ => "unknown",
})
}),
"width" => imagesize::blob_size(bytes)
.ok()
.map(|s| Variable::Integer(s.width as i64)),
"height" => imagesize::blob_size(bytes)
.ok()
.map(|s| Variable::Integer(s.height as i64)),
"area" => imagesize::blob_size(bytes)
.ok()
.map(|s| Variable::Integer(s.width.saturating_mul(s.height) as i64)),
"dimension" => imagesize::blob_size(bytes)
.ok()
.map(|s| Variable::Integer(s.width.saturating_add(s.height) as i64)),
_ => None,
}
})
.unwrap_or_default()
}
+116
View File
@@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{net::IpAddr, str::FromStr};
use mail_auth::common::resolver::ToReverseName;
use registry::types::ipmask::IpAddrOrMask;
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use sieve::{Context, runtime::Variable};
use utils::HexEncode;
use super::ApplyString;
pub fn fn_is_empty<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.is_empty(),
Variable::Integer(_) | Variable::Float(_) => false,
Variable::Array(a) => a.is_empty(),
}
.into()
}
pub fn fn_is_number<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
matches!(&v[0], Variable::Integer(_) | Variable::Float(_)).into()
}
pub fn fn_is_ip_addr<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string().parse::<std::net::IpAddr>().is_ok().into()
}
pub fn fn_is_ipv4_addr<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| matches!(ip, IpAddr::V4(_)))
.into()
}
pub fn fn_is_ipv6_addr<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| matches!(ip, IpAddr::V6(_)))
.into()
}
pub fn fn_is_ip_in_cidr<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let Ok(ip) = v[0].to_string().parse::<IpAddr>() else {
return false.into();
};
IpAddrOrMask::from_str(v[1].to_string().as_ref())
.map(|mask| mask.matches(&ip))
.unwrap_or(false)
.into()
}
pub fn fn_ip_reverse_name<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.parse::<std::net::IpAddr>()
.map(|ip| ip.to_reverse_name())
.unwrap_or_default()
.into()
}
pub fn fn_detect_file_type<'x>(ctx: &'x Context<'x>, v: Vec<Variable>) -> Variable {
ctx.message()
.part(ctx.part())
.and_then(|p| infer::get(p.contents()))
.map(|t| {
Variable::from(
if v[0].to_string() != "ext" {
t.mime_type()
} else {
t.extension()
}
.to_string(),
)
})
.unwrap_or_default()
}
pub fn fn_hash<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
use sha1::Digest;
let hash = v[1].to_string();
v[0].transform(|value| match hash.as_ref() {
"md5" => format!("{:x}", md5::compute(value.as_bytes())).into(),
"sha1" => {
let mut hasher = Sha1::new();
hasher.update(value.as_bytes());
hasher.finalize().hex_encode().into()
}
"sha256" => {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
hasher.finalize().hex_encode().into()
}
"sha512" => {
let mut hasher = Sha512::new();
hasher.update(value.as_bytes());
hasher.finalize().hex_encode().into()
}
_ => Variable::default(),
})
}
pub fn fn_get_var_names<'x>(ctx: &'x Context<'x>, _: Vec<Variable>) -> Variable {
Variable::Array(
ctx.global_variable_names()
.map(|v| Variable::from(v.to_uppercase()))
.collect::<Vec<_>>()
.into(),
)
}
+156
View File
@@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod array;
mod email;
mod header;
pub mod image;
pub mod misc;
pub mod text;
pub mod unicode;
pub mod url;
use sieve::{FunctionMap, runtime::Variable};
use self::{array::*, email::*, header::*, image::*, misc::*, text::*, unicode::*, url::*};
pub fn register_functions_trusted() -> FunctionMap {
FunctionMap::new()
.with_function("trim", fn_trim)
.with_function("trim_start", fn_trim_start)
.with_function("trim_end", fn_trim_end)
.with_function("len", fn_len)
.with_function("count", fn_count)
.with_function("is_empty", fn_is_empty)
.with_function("is_number", fn_is_number)
.with_function("is_ascii", fn_is_ascii)
.with_function("to_lowercase", fn_to_lowercase)
.with_function("to_uppercase", fn_to_uppercase)
.with_function("detect_language", fn_detect_language)
.with_function("is_email", fn_is_email)
.with_function("thread_name", fn_thread_name)
.with_function("html_to_text", fn_html_to_text)
.with_function("is_uppercase", fn_is_uppercase)
.with_function("is_lowercase", fn_is_lowercase)
.with_function("has_digits", fn_has_digits)
.with_function("count_spaces", fn_count_spaces)
.with_function("count_uppercase", fn_count_uppercase)
.with_function("count_lowercase", fn_count_lowercase)
.with_function("count_chars", fn_count_chars)
.with_function("dedup", fn_dedup)
.with_function("lines", fn_lines)
.with_function("is_header_utf8_valid", fn_is_header_utf8_valid)
.with_function("img_metadata", fn_img_metadata)
.with_function("is_ip_addr", fn_is_ip_addr)
.with_function("is_ipv4_addr", fn_is_ipv4_addr)
.with_function("is_ipv6_addr", fn_is_ipv6_addr)
.with_function("ip_reverse_name", fn_ip_reverse_name)
.with_function_args("is_ip_in_cidr", fn_is_ip_in_cidr, 2)
.with_function("winnow", fn_winnow)
.with_function("has_zwsp", fn_has_zwsp)
.with_function("has_obscured", fn_has_obscured)
.with_function("is_mixed_charset", fn_is_mixed_charset)
.with_function("puny_decode", fn_puny_decode)
.with_function("unicode_skeleton", fn_unicode_skeleton)
.with_function("cure_text", fn_cure_text)
.with_function("detect_file_type", fn_detect_file_type)
.with_function_args("sort", fn_sort, 2)
.with_function_args("email_part", fn_email_part, 2)
.with_function_args("eq_ignore_case", fn_eq_ignore_case, 2)
.with_function_args("contains", fn_contains, 2)
.with_function_args("contains_ignore_case", fn_contains_ignore_case, 2)
.with_function_args("starts_with", fn_starts_with, 2)
.with_function_args("ends_with", fn_ends_with, 2)
.with_function_args("received_part", fn_received_part, 2)
.with_function_args("cosine_similarity", fn_cosine_similarity, 2)
.with_function_args("jaccard_similarity", fn_jaccard_similarity, 2)
.with_function_args("levenshtein_distance", fn_levenshtein_distance, 2)
.with_function_args("uri_part", fn_uri_part, 2)
.with_function_args("substring", fn_substring, 3)
.with_function_args("split", fn_split, 2)
.with_function_args("rsplit", fn_rsplit, 2)
.with_function_args("split_once", fn_split_once, 2)
.with_function_args("rsplit_once", fn_rsplit_once, 2)
.with_function_args("split_n", fn_split_n, 3)
.with_function_args("strip_prefix", fn_strip_prefix, 2)
.with_function_args("strip_suffix", fn_strip_suffix, 2)
.with_function_args("is_intersect", fn_is_intersect, 2)
.with_function_args("hash", fn_hash, 2)
.with_function_no_args("is_encoding_problem", fn_is_encoding_problem)
.with_function_no_args("is_attachment", fn_is_attachment)
.with_function_no_args("is_body", fn_is_body)
.with_function_no_args("var_names", fn_get_var_names)
.with_function_no_args("attachment_name", fn_attachment_name)
.with_function_no_args("mime_part_len", fn_mime_part_len)
}
pub fn register_functions_untrusted() -> FunctionMap {
FunctionMap::new()
.with_function("trim", fn_trim)
.with_function("trim_start", fn_trim_start)
.with_function("trim_end", fn_trim_end)
.with_function("len", fn_len)
.with_function("count", fn_count)
.with_function("is_empty", fn_is_empty)
.with_function("is_number", fn_is_number)
.with_function("is_ascii", fn_is_ascii)
.with_function("to_lowercase", fn_to_lowercase)
.with_function("to_uppercase", fn_to_uppercase)
.with_function("is_email", fn_is_email)
.with_function("thread_name", fn_thread_name)
.with_function("html_to_text", fn_html_to_text)
.with_function("is_uppercase", fn_is_uppercase)
.with_function("is_lowercase", fn_is_lowercase)
.with_function("has_digits", fn_has_digits)
.with_function("count_spaces", fn_count_spaces)
.with_function("count_uppercase", fn_count_uppercase)
.with_function("count_lowercase", fn_count_lowercase)
.with_function("count_chars", fn_count_chars)
.with_function("dedup", fn_dedup)
.with_function("lines", fn_lines)
.with_function("is_ip_addr", fn_is_ip_addr)
.with_function("is_ipv4_addr", fn_is_ipv4_addr)
.with_function("is_ipv6_addr", fn_is_ipv6_addr)
.with_function("winnow", fn_winnow)
.with_function_args("sort", fn_sort, 2)
.with_function_args("email_part", fn_email_part, 2)
.with_function_args("eq_ignore_case", fn_eq_ignore_case, 2)
.with_function_args("contains", fn_contains, 2)
.with_function_args("contains_ignore_case", fn_contains_ignore_case, 2)
.with_function_args("starts_with", fn_starts_with, 2)
.with_function_args("ends_with", fn_ends_with, 2)
.with_function_args("uri_part", fn_uri_part, 2)
.with_function_args("substring", fn_substring, 3)
.with_function_args("split", fn_split, 2)
.with_function_args("rsplit", fn_rsplit, 2)
.with_function_args("split_once", fn_split_once, 2)
.with_function_args("rsplit_once", fn_rsplit_once, 2)
.with_function_args("split_n", fn_split_n, 3)
.with_function_args("strip_prefix", fn_strip_prefix, 2)
.with_function_args("strip_suffix", fn_strip_suffix, 2)
.with_function_args("is_intersect", fn_is_intersect, 2)
}
pub trait ApplyString<'x> {
fn transform(&self, f: impl Fn(&'_ str) -> Variable) -> Variable;
}
impl ApplyString<'_> for Variable {
fn transform(&self, f: impl Fn(&'_ str) -> Variable) -> Variable {
match self {
Variable::String(s) => f(s),
Variable::Array(list) => list
.iter()
.map(|v| match v {
Variable::String(s) => f(s),
v => f(v.to_string().as_ref()),
})
.collect::<Vec<_>>()
.into(),
v => f(v.to_string().as_ref()),
}
}
}
+317
View File
@@ -0,0 +1,317 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::decoders::html::html_to_text;
use sieve::{Context, runtime::Variable};
use super::ApplyString;
pub fn fn_trim<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| Variable::from(s.trim()))
}
pub fn fn_trim_end<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| Variable::from(s.trim_end()))
}
pub fn fn_trim_start<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| Variable::from(s.trim_start()))
}
pub fn fn_len<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.len(),
Variable::Array(a) => a.len(),
v => v.to_string().len(),
}
.into()
}
pub fn fn_to_lowercase<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| Variable::from(s.to_lowercase()))
}
pub fn fn_to_uppercase<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| Variable::from(s.to_uppercase()))
}
pub fn fn_is_uppercase<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| {
s.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_uppercase())
.into()
})
}
pub fn fn_is_lowercase<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| {
s.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_lowercase())
.into()
})
}
pub fn fn_has_digits<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|s| s.chars().any(|c| c.is_ascii_digit()).into())
}
pub fn tokenize_words(v: &Variable) -> Variable {
v.to_string()
.split_whitespace()
.filter(|word| word.chars().all(|c| c.is_alphanumeric()))
.map(|word| Variable::from(word.to_string()))
.collect::<Vec<_>>()
.into()
}
pub fn fn_count_spaces<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_ref()
.chars()
.filter(|c| c.is_whitespace())
.count()
.into()
}
pub fn fn_count_uppercase<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_ref()
.chars()
.filter(|c| c.is_alphabetic() && c.is_uppercase())
.count()
.into()
}
pub fn fn_count_lowercase<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_ref()
.chars()
.filter(|c| c.is_alphabetic() && c.is_lowercase())
.count()
.into()
}
pub fn fn_count_chars<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string().as_ref().chars().count().into()
}
pub fn fn_eq_ignore_case<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.eq_ignore_ascii_case(v[1].to_string().as_ref())
.into()
}
pub fn fn_contains<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.contains(v[1].to_string().as_ref()),
Variable::Array(arr) => arr.contains(&v[1]),
val => val.to_string().contains(v[1].to_string().as_ref()),
}
.into()
}
pub fn fn_contains_ignore_case<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let needle = v[1].to_string();
match &v[0] {
Variable::String(s) => s.to_lowercase().contains(&needle.to_lowercase()),
Variable::Array(arr) => arr.iter().any(|v| match v {
Variable::String(s) => s.eq_ignore_ascii_case(needle.as_ref()),
_ => false,
}),
val => val.to_string().contains(needle.as_ref()),
}
.into()
}
pub fn fn_starts_with<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.starts_with(v[1].to_string().as_ref())
.into()
}
pub fn fn_ends_with<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string().ends_with(v[1].to_string().as_ref()).into()
}
pub fn fn_lines<'x>(_: &'x Context<'x>, mut v: Vec<Variable>) -> Variable {
match v.remove(0) {
Variable::String(s) => s
.lines()
.map(|s| Variable::from(s.to_string()))
.collect::<Vec<_>>()
.into(),
val => val,
}
}
pub fn fn_substring<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.chars()
.skip(v[1].to_usize())
.take(v[2].to_usize())
.collect::<String>()
.into()
}
pub fn fn_strip_prefix<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let prefix = v[1].to_string();
v[0].transform(|s| {
s.strip_prefix(prefix.as_ref())
.map(Variable::from)
.unwrap_or_default()
})
}
pub fn fn_strip_suffix<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let suffix = v[1].to_string();
v[0].transform(|s| {
s.strip_suffix(suffix.as_ref())
.map(Variable::from)
.unwrap_or_default()
})
}
pub fn fn_split<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.split(v[1].to_string().as_ref())
.map(|s| Variable::from(s.to_string()))
.collect::<Vec<_>>()
.into()
}
pub fn fn_rsplit<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.rsplit(v[1].to_string().as_ref())
.map(|s| Variable::from(s.to_string()))
.collect::<Vec<_>>()
.into()
}
pub fn fn_split_n<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let value = v[0].to_string();
let arg = v[1].to_string();
let num = v[2].to_integer() as usize;
let mut result = Vec::new();
let mut s = value.as_ref();
for _ in 0..num {
if let Some((a, b)) = s.split_once(arg.as_ref()) {
result.push(Variable::from(a.to_string()));
s = b;
} else {
break;
}
}
result.push(Variable::from(s.to_string()));
result.into()
}
pub fn fn_split_once<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.split_once(v[1].to_string().as_ref())
.map(|(a, b)| {
Variable::Array(
vec![Variable::from(a.to_string()), Variable::from(b.to_string())].into(),
)
})
.unwrap_or_default()
}
pub fn fn_rsplit_once<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].to_string()
.rsplit_once(v[1].to_string().as_ref())
.map(|(a, b)| {
Variable::Array(
vec![Variable::from(a.to_string()), Variable::from(b.to_string())].into(),
)
})
.unwrap_or_default()
}
/**
* `levenshtein-rs` - levenshtein
*
* MIT licensed.
*
* Copyright (c) 2016 Titus Wormer <[email protected]>
*/
pub fn fn_levenshtein_distance<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let a = v[0].to_string();
let b = v[1].to_string();
levenshtein_distance(a.as_ref(), b.as_ref()).into()
}
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
let mut result = 0;
/* Shortcut optimizations / degenerate cases. */
if a == b {
return result;
}
let length_a = a.chars().count();
let length_b = b.chars().count();
if length_a == 0 {
return length_b;
} else if length_b == 0 {
return length_a;
}
/* Initialize the vector.
*
* This is why its fast, normally a matrix is used,
* here we use a single vector. */
let mut cache: Vec<usize> = (1..).take(length_a).collect();
let mut distance_a;
let mut distance_b;
/* Loop. */
for (index_b, code_b) in b.chars().enumerate() {
result = index_b;
distance_a = index_b;
for (index_a, code_a) in a.chars().enumerate() {
distance_b = if code_a == code_b {
distance_a
} else {
distance_a + 1
};
distance_a = cache[index_a];
result = if distance_a > result {
if distance_b > result {
result + 1
} else {
distance_b
}
} else if distance_b > distance_a {
distance_a + 1
} else {
distance_b
};
cache[index_a] = result;
}
}
result
}
pub fn fn_detect_language<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
whatlang::detect_lang(v[0].to_string().as_ref())
.map(|l| l.code())
.unwrap_or("unknown")
.into()
}
pub fn fn_html_to_text<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
html_to_text(v[0].to_string().as_ref()).into()
}
@@ -0,0 +1,92 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::{Context, runtime::Variable};
use crate::scripts::IsMixedCharset;
pub fn fn_is_ascii<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.is_ascii(),
Variable::Integer(_) | Variable::Float(_) => true,
Variable::Array(a) => a.iter().all(|v| match v {
Variable::String(s) => s.is_ascii(),
_ => true,
}),
}
.into()
}
pub fn fn_has_zwsp<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.chars().any(|c| c.is_zwsp()),
Variable::Array(a) => a.iter().any(|v| match v {
Variable::String(s) => s.chars().any(|c| c.is_zwsp()),
_ => true,
}),
Variable::Integer(_) | Variable::Float(_) => false,
}
.into()
}
pub fn fn_has_obscured<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.chars().any(|c| c.is_obscured()),
Variable::Array(a) => a.iter().any(|v| match v {
Variable::String(s) => s.chars().any(|c| c.is_obscured()),
_ => true,
}),
Variable::Integer(_) | Variable::Float(_) => false,
}
.into()
}
pub trait CharUtils {
fn is_zwsp(&self) -> bool;
fn is_obscured(&self) -> bool;
}
impl CharUtils for char {
fn is_zwsp(&self) -> bool {
matches!(
self,
'\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{FEFF}' | '\u{00AD}'
)
}
fn is_obscured(&self) -> bool {
matches!(
self,
'\u{200B}'..='\u{200F}'
| '\u{2028}'..='\u{202F}'
| '\u{205F}'..='\u{206F}'
| '\u{FEFF}'
)
}
}
pub fn fn_cure_text<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
decancer::cure(v[0].to_string().as_ref(), decancer::Options::default())
.map(String::from)
.unwrap_or_default()
.into()
}
pub fn fn_unicode_skeleton<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
unicode_security::skeleton(v[0].to_string().as_ref())
.collect::<String>()
.into()
}
pub fn fn_is_mixed_charset<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let text = v[0].to_string();
if !text.is_empty() {
text.as_ref().is_mixed_charset()
} else {
false
}
.into()
}
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use hyper::Uri;
use sieve::{Context, runtime::Variable};
use super::ApplyString;
pub fn fn_uri_part<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
let part = v[1].to_string();
v[0].transform(|uri| {
uri.parse::<Uri>()
.ok()
.and_then(|uri| match part.as_ref() {
"scheme" => uri.scheme_str().map(|s| Variable::from(s.to_string())),
"host" => uri.host().map(|s| Variable::from(s.to_string())),
"scheme_host" => uri
.scheme_str()
.and_then(|s| (s, uri.host()?).into())
.map(|(s, h)| Variable::from(format!("{}://{}", s, h))),
"path" => Variable::from(uri.path().to_string()).into(),
"port" => uri.port_u16().map(|port| Variable::Integer(port as i64)),
"query" => uri.query().map(|s| Variable::from(s.to_string())),
"path_query" => uri.path_and_query().map(|s| Variable::from(s.to_string())),
"authority" => uri.authority().map(|s| Variable::from(s.to_string())),
_ => None,
})
.unwrap_or_default()
})
}
pub fn fn_puny_decode<'x>(_: &'x Context<'x>, v: Vec<Variable>) -> Variable {
v[0].transform(|domain| {
if domain.contains("xn--") {
let mut decoded = String::with_capacity(domain.len());
for part in domain.split('.') {
if !decoded.is_empty() {
decoded.push('.');
}
if let Some(puny) = part
.strip_prefix("xn--")
.and_then(idna::punycode::decode_to_string)
{
decoded.push_str(&puny);
} else {
decoded.push_str(part);
}
}
decoded.into()
} else {
domain.into()
}
})
}
+77
View File
@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use sieve::{Envelope, runtime::Variable};
use store::Value;
use unicode_security::mixed_script::AugmentedScriptSet;
use crate::IntoString;
pub mod functions;
pub mod plugins;
#[derive(Debug, serde::Serialize)]
#[serde(tag = "action")]
#[serde(rename_all = "camelCase")]
pub enum ScriptModification {
SetEnvelope {
name: Envelope,
value: String,
},
AddHeader {
name: Arc<String>,
value: Arc<String>,
},
}
pub fn into_sieve_value(value: Value) -> Variable {
match value {
Value::Integer(v) => Variable::Integer(v),
Value::Bool(v) => Variable::Integer(i64::from(v)),
Value::Float(v) => Variable::Float(v),
Value::Text(v) => Variable::String(v.into_owned().into()),
Value::Blob(v) => Variable::String(v.into_owned().into_string().into()),
Value::Null => Variable::default(),
}
}
pub fn into_store_value(value: Variable) -> Value<'static> {
match value {
Variable::String(v) => Value::Text(v.to_string().into()),
Variable::Integer(v) => Value::Integer(v),
Variable::Float(v) => Value::Float(v),
v => Value::Text(v.to_string().into_owned().into()),
}
}
pub fn to_store_value(value: &Variable) -> Value<'static> {
match value {
Variable::String(v) => Value::Text(v.to_string().into()),
Variable::Integer(v) => Value::Integer(*v),
Variable::Float(v) => Value::Float(*v),
v => Value::Text(v.to_string().into_owned().into()),
}
}
pub trait IsMixedCharset {
fn is_mixed_charset(&self) -> bool;
}
impl<T: AsRef<str>> IsMixedCharset for T {
fn is_mixed_charset(&self) -> bool {
let mut set: Option<AugmentedScriptSet> = None;
for ch in self.as_ref().chars() {
if !ch.is_ascii() {
set.get_or_insert_default().intersect_with(ch.into());
}
}
set.is_some_and(|set| set.is_empty())
}
}
+204
View File
@@ -0,0 +1,204 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::net::IpAddr;
use mail_auth::IpLookupStrategy;
use sieve::{FunctionMap, runtime::Variable};
use super::PluginContext;
pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("dns_query", plugin_id, 2);
}
pub fn register_exists(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("dns_exists", plugin_id, 2);
}
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let entry = ctx.arguments[0].to_string();
let record_type = ctx.arguments[1].to_string();
Ok(if record_type.eq_ignore_ascii_case("ip") {
match ctx
.server
.core
.smtp
.resolvers
.dns
.ip_lookup(
entry.as_ref(),
IpLookupStrategy::Ipv4thenIpv6,
10,
Some(&ctx.server.inner.cache.dns_ipv4),
Some(&ctx.server.inner.cache.dns_ipv6),
)
.await
{
Ok(result) => result
.iter()
.map(|ip| Variable::from(ip.to_string()))
.collect::<Vec<_>>()
.into(),
Err(err) => err.short_error().into(),
}
} else if record_type.eq_ignore_ascii_case("mx") {
match ctx
.server
.core
.smtp
.resolvers
.dns
.mx_lookup(entry.as_ref(), Some(&ctx.server.inner.cache.dns_mx))
.await
{
Ok(result) => result
.rrset
.iter()
.flat_map(|mx| {
mx.exchanges
.iter()
.map(|host| Variable::from(format!("{} {}", mx.preference, host)))
})
.collect::<Vec<_>>()
.into(),
Err(err) => err.short_error().into(),
}
} else if record_type.eq_ignore_ascii_case("txt") {
#[cfg(feature = "test_mode")]
{
if entry.contains("origin") {
return Ok(Variable::from("23028|US|arin|2002-01-04".to_string()));
}
}
match ctx
.server
.core
.smtp
.resolvers
.dns
.txt_raw_lookup(entry.as_ref())
.await
{
Ok(result) => Variable::from(String::from_utf8(result).unwrap_or_default()),
Err(err) => err.short_error().into(),
}
} else if record_type.eq_ignore_ascii_case("ptr") {
if let Ok(addr) = entry.parse::<IpAddr>() {
match ctx
.server
.core
.smtp
.resolvers
.dns
.ptr_lookup(addr, Some(&ctx.server.inner.cache.dns_ptr))
.await
{
Ok(result) => result
.rrset
.iter()
.map(|host| Variable::from(host.to_string()))
.collect::<Vec<_>>()
.into(),
Err(err) => err.short_error().into(),
}
} else {
Variable::default()
}
} else if record_type.eq_ignore_ascii_case("ipv4") {
#[cfg(feature = "test_mode")]
{
if entry.contains(".168.192.") {
let parts = entry.split('.').collect::<Vec<_>>();
return Ok(vec![Variable::from(format!("127.0.{}.{}", parts[1], parts[0]))].into());
}
}
match ctx
.server
.core
.smtp
.resolvers
.dns
.ipv4_lookup(entry.as_ref(), Some(&ctx.server.inner.cache.dns_ipv4))
.await
{
Ok(result) => result
.rrset
.iter()
.map(|ip| Variable::from(ip.to_string()))
.collect::<Vec<_>>()
.into(),
Err(err) => err.short_error().into(),
}
} else if record_type.eq_ignore_ascii_case("ipv6") {
match ctx
.server
.core
.smtp
.resolvers
.dns
.ipv6_lookup(entry.as_ref(), Some(&ctx.server.inner.cache.dns_ipv6))
.await
{
Ok(result) => result
.rrset
.iter()
.map(|ip| Variable::from(ip.to_string()))
.collect::<Vec<_>>()
.into(),
Err(err) => err.short_error().into(),
}
} else {
Variable::default()
})
}
pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let entry = ctx.arguments[0].to_string();
let record_type = ctx.arguments[1].to_string();
let result = if record_type.eq_ignore_ascii_case("ip") {
ctx.server.dns_exists_ip(entry.as_ref()).await
} else if record_type.eq_ignore_ascii_case("mx") {
ctx.server.dns_exists_mx(entry.as_ref()).await
} else if record_type.eq_ignore_ascii_case("ptr") {
ctx.server.dns_exists_ptr(entry.as_ref()).await
} else if record_type.eq_ignore_ascii_case("ipv4") {
#[cfg(feature = "test_mode")]
{
if entry.starts_with("2.0.168.192.") {
return Ok(1.into());
}
}
ctx.server.dns_exists_ipv4(entry.as_ref()).await
} else if record_type.eq_ignore_ascii_case("ipv6") {
ctx.server.dns_exists_ipv6(entry.as_ref()).await
} else {
return Ok((-1).into());
};
Ok(result.map(i64::from).unwrap_or(-1).into())
}
trait ShortError {
fn short_error(&self) -> &'static str;
}
impl ShortError for mail_auth::Error {
fn short_error(&self) -> &'static str {
match self {
mail_auth::Error::Dns(mail_auth::DnsError::Resolver(_)) => "temp_fail",
mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_)) => "not_found",
mail_auth::Error::Io(_) => "io_error",
mail_auth::Error::Dns(mail_auth::DnsError::InvalidRecordType) => "invalid_record",
_ => "unknown_error",
}
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::process::Command;
use sieve::{FunctionMap, runtime::Variable};
use super::PluginContext;
pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("exec", plugin_id, 2);
}
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let mut arguments = ctx.arguments.into_iter();
tokio::task::spawn_blocking(move || {
let command = arguments
.next()
.map(|a| a.to_string().into_owned())
.unwrap_or_default();
match Command::new(&command)
.args(
arguments
.next()
.map(|a| a.into_string_array())
.unwrap_or_default(),
)
.output()
{
Ok(result) => Ok(result.status.success()),
Err(err) => Err(trc::SieveEvent::RuntimeError
.ctx(trc::Key::Path, command)
.reason(err)
.details("Failed to execute command")),
}
})
.await
.map_err(|err| {
trc::EventType::Server(trc::ServerEvent::ThreadError)
.reason(err)
.caused_by(trc::location!())
.details("Join Error")
})?
.map(Into::into)
}
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::{FunctionMap, runtime::Variable};
use crate::scripts::ScriptModification;
use super::PluginContext;
pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("add_header", plugin_id, 2);
}
pub fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(if let (Variable::String(name), Variable::String(value)) =
(&ctx.arguments[0], &ctx.arguments[1])
{
ctx.modifications.push(ScriptModification::AddHeader {
name: name.clone(),
value: value.clone(),
});
true
} else {
false
}
.into())
}
+52
View File
@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Duration;
use reqwest::header::USER_AGENT;
use sieve::{FunctionMap, runtime::Variable};
use super::PluginContext;
pub fn register_header(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("http_header", plugin_id, 4);
}
pub async fn exec_header(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let url = ctx.arguments[0].to_string();
let header = ctx.arguments[1].to_string();
let agent = ctx.arguments[2].to_string();
let timeout = ctx.arguments[3].to_string().parse::<u64>().unwrap_or(5000);
#[cfg(feature = "test_mode")]
if url.contains("redirect.") {
return Ok(Variable::from(url.split_once("/?").unwrap().1.to_string()));
}
ctx.server
.core
.sieve
.http_client
.get(url.as_ref())
.header(USER_AGENT, agent.as_ref())
.timeout(Duration::from_millis(timeout))
.send()
.await
.map_err(|err| {
trc::SieveEvent::RuntimeError
.into_err()
.reason(err)
.details("Failed to send request")
})
.map(|response| {
response
.headers()
.get(header.as_ref())
.and_then(|h| h.to_str().ok())
.map(|h| Variable::from(h.to_string()))
.unwrap_or_default()
})
}
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::{FunctionMap, compiler::Number, runtime::Variable};
use std::time::Instant;
use trc::{AiEvent, SecurityEvent};
use super::PluginContext;
pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("llm_prompt", plugin_id, 3);
}
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(false.into())
}
+145
View File
@@ -0,0 +1,145 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::PluginContext;
use crate::scripts::into_sieve_value;
use sieve::{FunctionMap, runtime::Variable};
use store::{Deserialize, Value, dispatch::lookup::KeyValue};
pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("key_exists", plugin_id, 2);
}
pub fn register_get(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("key_get", plugin_id, 2);
}
pub fn register_set(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("key_set", plugin_id, 4);
}
pub fn register_local_domain(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("is_local_domain", plugin_id, 1);
}
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.server.get_lookup_store(v.as_str()),
_ => Some(ctx.server.core.storage.memory.clone()),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
.ctx(trc::Key::Id, ctx.arguments[0].to_string().into_owned())
.details("Unknown store")
})?;
Ok(match &ctx.arguments[1] {
Variable::Array(items) => {
for item in items.iter() {
if !item.is_empty() && store.key_exists(item.to_string()).await? {
return Ok(true.into());
}
}
false
}
v if !v.is_empty() => store.key_exists(v.to_string()).await?,
_ => false,
}
.into())
}
pub async fn exec_get(ctx: PluginContext<'_>) -> trc::Result<Variable> {
match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.server.get_lookup_store(v.as_str()),
_ => Some(ctx.server.core.storage.memory.clone()),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
.ctx(trc::Key::Id, ctx.arguments[0].to_string().into_owned())
.details("Unknown store")
})?
.key_get::<VariableWrapper>(ctx.arguments[1].to_string())
.await
.map(|v| v.map(|v| v.into_inner()).unwrap_or_default())
}
pub async fn exec_set(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let expires = match &ctx.arguments[3] {
Variable::Integer(v) => Some(*v as u64),
Variable::Float(v) => Some(*v as u64),
_ => None,
};
match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx.server.get_lookup_store(v.as_str()),
_ => Some(ctx.server.core.storage.memory.clone()),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
.ctx(trc::Key::Id, ctx.arguments[0].to_string().into_owned())
.details("Unknown store")
})?
.key_set(
KeyValue::new(
ctx.arguments[1].to_string().into_owned().into_bytes(),
if !ctx.arguments[2].is_empty() {
bincode::serde::encode_to_vec(&ctx.arguments[2], bincode::config::standard())
.unwrap_or_default()
} else {
vec![]
},
)
.expires_opt(expires),
)
.await
.map(|_| true.into())
}
pub async fn exec_local_domain(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let domain = ctx.arguments[0].to_string();
if !domain.is_empty() {
ctx.server
.domain(domain.as_ref())
.await
.map(|result| Variable::from(result.is_some()))
} else {
Ok(Variable::default())
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VariableWrapper(Variable);
impl Deserialize for VariableWrapper {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Ok(VariableWrapper(
bincode::serde::decode_from_slice::<Variable, _>(bytes, bincode::config::standard())
.map(|v| v.0)
.unwrap_or_else(|_| {
Variable::String(String::from_utf8_lossy(bytes).into_owned().into())
}),
))
}
}
impl From<i64> for VariableWrapper {
fn from(value: i64) -> Self {
VariableWrapper(value.into())
}
}
impl VariableWrapper {
pub fn into_inner(self) -> Variable {
self.0
}
}
impl From<Value<'static>> for VariableWrapper {
fn from(value: Value<'static>) -> Self {
VariableWrapper(into_sieve_value(value))
}
}
+113
View File
@@ -0,0 +1,113 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod dns;
pub mod exec;
pub mod headers;
pub mod http;
pub mod llm_prompt;
pub mod lookup;
pub mod query;
pub mod text;
use mail_parser::Message;
use sieve::{FunctionMap, Input, runtime::Variable};
use crate::{Core, Server, auth::AccessToken};
use super::ScriptModification;
type RegisterPluginFnc = fn(u32, &mut FunctionMap) -> ();
pub struct PluginContext<'x> {
pub session_id: u64,
pub access_token: Option<&'x AccessToken>,
pub server: &'x Server,
pub message: &'x Message<'x>,
pub modifications: &'x mut Vec<ScriptModification>,
pub arguments: Vec<Variable>,
}
const PLUGINS_REGISTER: [RegisterPluginFnc; 13] = [
query::register,
exec::register,
lookup::register,
lookup::register_get,
lookup::register_set,
lookup::register_local_domain,
dns::register,
dns::register_exists,
http::register_header,
headers::register,
text::register_tokenize,
text::register_domain_part,
llm_prompt::register,
];
pub trait RegisterSievePlugins {
fn register_plugins_trusted(self) -> Self;
fn register_plugins_untrusted(self) -> Self;
}
impl RegisterSievePlugins for FunctionMap {
fn register_plugins_trusted(mut self) -> Self {
#[cfg(feature = "test_mode")]
{
self.set_external_function("print", PLUGINS_REGISTER.len() as u32, 1)
}
for (i, fnc) in PLUGINS_REGISTER.iter().enumerate() {
fnc(i as u32, &mut self);
}
self
}
fn register_plugins_untrusted(mut self) -> Self {
llm_prompt::register(12, &mut self);
self
}
}
impl Core {
pub async fn run_plugin(&self, id: u32, ctx: PluginContext<'_>) -> Input {
#[cfg(feature = "test_mode")]
if id == PLUGINS_REGISTER.len() as u32 {
return test_print(ctx);
}
let session_id = ctx.session_id;
let result = match id {
0 => query::exec(ctx).await,
1 => exec::exec(ctx).await,
2 => lookup::exec(ctx).await,
3 => lookup::exec_get(ctx).await,
4 => lookup::exec_set(ctx).await,
5 => lookup::exec_local_domain(ctx).await,
6 => dns::exec(ctx).await,
7 => dns::exec_exists(ctx).await,
8 => http::exec_header(ctx).await,
9 => headers::exec(ctx),
10 => text::exec_tokenize(ctx),
11 => text::exec_domain_part(ctx),
12 => llm_prompt::exec(ctx).await,
_ => unreachable!(),
};
match result {
Ok(result) => result.into(),
Err(err) => {
trc::error!(err.span_id(session_id).details("Sieve runtime error"));
Input::FncResult(Variable::default())
}
}
}
}
#[cfg(feature = "test_mode")]
pub fn test_print(ctx: PluginContext<'_>) -> Input {
println!("{}", ctx.arguments[0].to_string());
Input::True
}
@@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::PluginContext;
use crate::scripts::{into_sieve_value, to_store_value};
use sieve::{FunctionMap, runtime::Variable};
use std::cmp::Ordering;
use store::{Rows, Value};
pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("query", plugin_id, 3);
}
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> {
// Obtain store name
let store = match &ctx.arguments[0] {
Variable::String(v) if !v.is_empty() => ctx
.server
.get_lookup_store(v.as_str())
.and_then(|v| v.into_store()),
_ => Some(ctx.server.core.storage.data.clone()),
}
.ok_or_else(|| {
trc::SieveEvent::RuntimeError
.ctx(trc::Key::Id, ctx.arguments[0].to_string().into_owned())
.details("Unknown store")
})?;
// Obtain query string
let query = ctx.arguments[1].to_string();
if query.is_empty() {
trc::bail!(
trc::SieveEvent::RuntimeError
.ctx(trc::Key::Id, ctx.arguments[0].to_string().into_owned())
.details("Empty query string")
);
}
// Obtain arguments
let arguments = match &ctx.arguments[2] {
Variable::Array(l) => l.iter().map(to_store_value).collect(),
v => vec![to_store_value(v)],
};
// Run query
if query
.as_bytes()
.get(..6)
.is_some_and(|q| q.eq_ignore_ascii_case(b"SELECT"))
{
let mut rows = store.sql_query::<Rows>(&query, arguments).await?;
Ok(match rows.rows.len().cmp(&1) {
Ordering::Equal => {
let mut row = rows.rows.pop().unwrap().values;
match row.len().cmp(&1) {
Ordering::Equal if !matches!(row.first(), Some(Value::Null)) => {
row.pop().map(into_sieve_value).unwrap()
}
Ordering::Less => Variable::default(),
_ => Variable::Array(
row.into_iter()
.map(into_sieve_value)
.collect::<Vec<_>>()
.into(),
),
}
}
Ordering::Less => Variable::default(),
Ordering::Greater => rows
.rows
.into_iter()
.map(|r| {
Variable::Array(
r.values
.into_iter()
.map(into_sieve_value)
.collect::<Vec<_>>()
.into(),
)
})
.collect::<Vec<_>>()
.into(),
})
} else {
Ok(store
.sql_query::<usize>(&query, arguments)
.await
.is_ok()
.into())
}
}
+78
View File
@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use nlp::tokenizers::types::{TokenType, TypesTokenizer};
use sieve::{FunctionMap, runtime::Variable};
use crate::scripts::functions::{ApplyString, text::tokenize_words};
use super::PluginContext;
pub fn register_tokenize(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("tokenize", plugin_id, 2);
}
pub fn register_domain_part(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("domain_part", plugin_id, 2);
}
pub fn exec_tokenize(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let mut v = ctx.arguments;
let (urls, urls_without_scheme, emails) = match v[1].to_string().as_ref() {
"words" => return Ok(tokenize_words(&v[0])),
"uri" | "url" => (true, true, true),
"uri_strict" | "url_strict" => (true, false, false),
"email" => (false, false, true),
_ => return Ok(Variable::default()),
};
Ok(match v.remove(0) {
v @ (Variable::String(_) | Variable::Array(_)) => {
TypesTokenizer::new(v.to_string().as_ref())
.tokenize_numbers(false)
.tokenize_urls(urls)
.tokenize_urls_without_scheme(urls_without_scheme)
.tokenize_emails(emails)
.filter_map(|t| match t.word {
TokenType::Url(text) if urls => Variable::from(text.to_string()).into(),
TokenType::UrlNoScheme(text) if urls_without_scheme => {
Variable::from(format!("https://{text}")).into()
}
TokenType::Email(text) if emails => Variable::from(text.to_string()).into(),
_ => None,
})
.collect::<Vec<_>>()
.into()
}
v => v,
})
}
enum DomainPart {
Sld,
Tld,
Host,
}
pub fn exec_domain_part(ctx: PluginContext<'_>) -> trc::Result<Variable> {
let v = ctx.arguments;
let part = match v[1].to_string().as_ref() {
"sld" => DomainPart::Sld,
"tld" => DomainPart::Tld,
"host" => DomainPart::Host,
_ => return Ok(Variable::default()),
};
Ok(v[0].transform(|domain| {
match part {
DomainPart::Sld => psl::domain_str(domain),
DomainPart::Tld => domain.rsplit_once('.').map(|(_, tld)| tld),
DomainPart::Host => domain.split_once('.').map(|(host, _)| host),
}
.map(Variable::from)
.unwrap_or_default()
}))
}