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,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