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:
@@ -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()
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 it’s 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user