diff --git a/Cargo.lock b/Cargo.lock index 6f9036d9..93fc6277 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2985,11 +2985,11 @@ dependencies = [ [[package]] name = "librocksdb-sys" -version = "0.11.0+8.1.1" +version = "0.16.0+8.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" dependencies = [ - "bindgen 0.65.1", + "bindgen 0.69.4", "bzip2-sys", "cc", "glob", @@ -4664,9 +4664,9 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" dependencies = [ "libc", "librocksdb-sys", diff --git a/crates/smtp/src/core/eval.rs b/crates/smtp/src/core/eval.rs index f1d6bca6..c1022817 100644 --- a/crates/smtp/src/core/eval.rs +++ b/crates/smtp/src/core/eval.rs @@ -1,9 +1,10 @@ -use std::{borrow::Cow, net::IpAddr, sync::Arc, vec::IntoIter}; +use std::{borrow::Cow, cmp::Ordering, net::IpAddr, sync::Arc, vec::IntoIter}; use directory::Directory; use mail_auth::IpLookupStrategy; use sieve::Sieve; -use store::{Deserialize, LookupStore}; +use smtp_proto::IntoString; +use store::{Deserialize, LookupStore, Rows, Value}; use utils::{ config::if_block::IfBlock, expr::{Expression, Variable}, @@ -29,7 +30,8 @@ pub const F_IS_LOCAL_DOMAIN: u32 = 0; pub const F_IS_LOCAL_ADDRESS: u32 = 1; pub const F_KEY_GET: u32 = 2; pub const F_KEY_EXISTS: u32 = 3; -pub const F_DNS_QUERY: u32 = 4; +pub const F_SQL_QUERY: u32 = 4; +pub const F_DNS_QUERY: u32 = 5; pub const VARIABLES_MAP: &[(&str, u32)] = &[ ("rcpt", V_RECIPIENT), @@ -51,6 +53,7 @@ pub const FUNCTIONS_MAP: &[(&str, u32, u32)] = &[ ("key_get", F_KEY_GET, 2), ("key_exists", F_KEY_EXISTS, 2), ("dns_query", F_DNS_QUERY, 2), + ("sql_query", F_SQL_QUERY, 3), ]; impl SMTP { @@ -200,6 +203,7 @@ impl SMTP { .into() } F_DNS_QUERY => self.dns_query(params).await, + F_SQL_QUERY => self.sql_query(params).await, _ => Variable::default(), } } @@ -294,6 +298,65 @@ impl SMTP { }) } + async fn sql_query<'x>(&self, mut arguments: FncParams<'x>) -> Variable<'x> { + let store = self.get_lookup_store(arguments.next_as_string().as_ref()); + let query = arguments.next_as_string(); + + if query.is_empty() { + tracing::warn!( + context = "eval:sql_query", + event = "invalid", + reason = "Empty query string", + ); + return Variable::default(); + } + + // Obtain arguments + let arguments = match arguments.next() { + Variable::Array(l) => l.into_iter().map(to_store_value).collect(), + v => vec![to_store_value(v)], + }; + + // Run query + if query + .as_bytes() + .get(..6) + .map_or(false, |q| q.eq_ignore_ascii_case(b"SELECT")) + { + if let Ok(mut rows) = store.query::(&query, arguments).await { + 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_variable).unwrap() + } + Ordering::Less => Variable::default(), + _ => Variable::Array( + row.into_iter().map(into_variable).collect::>(), + ), + } + } + Ordering::Less => Variable::default(), + Ordering::Greater => rows + .rows + .into_iter() + .map(|r| { + Variable::Array( + r.values.into_iter().map(into_variable).collect::>(), + ) + }) + .collect::>() + .into(), + } + } else { + false.into() + } + } else { + store.query::(&query, arguments).await.is_ok().into() + } + } + async fn dns_query<'x>(&self, mut arguments: FncParams<'x>) -> Variable<'x> { let entry = arguments.next_as_string(); let record_type = arguments.next_as_string(); @@ -386,6 +449,10 @@ impl<'x> FncParams<'x> { pub fn next_as_string(&mut self) -> Cow<'x, str> { self.params.next().unwrap().into_string() } + + pub fn next(&mut self) -> Variable<'x> { + self.params.next().unwrap() + } } #[derive(Debug)] @@ -414,3 +481,23 @@ impl VariableWrapper { self.0 } } + +fn to_store_value(value: Variable) -> Value { + match value { + Variable::String(v) => Value::Text(v), + Variable::Integer(v) => Value::Integer(v), + Variable::Float(v) => Value::Float(v), + v => Value::Text(v.to_string().into_owned().into()), + } +} + +fn into_variable(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), + Value::Blob(v) => Variable::String(v.into_owned().into_string().into()), + Value::Null => Variable::default(), + } +} diff --git a/crates/smtp/src/scripts/plugins/query.rs b/crates/smtp/src/scripts/plugins/query.rs index f66cb012..da7bcde8 100644 --- a/crates/smtp/src/scripts/plugins/query.rs +++ b/crates/smtp/src/scripts/plugins/query.rs @@ -40,16 +40,20 @@ pub fn exec(ctx: PluginContext<'_>) -> Variable { let span = ctx.span; // Obtain store name - let store = ctx.arguments[0].to_string(); - let store = if let Some(store_) = ctx.core.shared.lookup_stores.get(store.as_ref()) { - store_ + let store = match &ctx.arguments[0] { + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), + }; + + let store = if let Some(store) = store { + store } else { tracing::warn!( parent: span, context = "sieve:query", event = "failed", reason = "Unknown store", - store = %store, + store = ctx.arguments[0].to_string().as_ref(), ); return false.into(); }; diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 804e1457..2a0a40fc 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -7,7 +7,7 @@ resolver = "2" [dependencies] utils = { path = "../utils" } nlp = { path = "../nlp" } -rocksdb = { version = "0.21", optional = true, features = ["multi-threaded-cf"] } +rocksdb = { version = "0.22", optional = true, features = ["multi-threaded-cf"] } foundationdb = { version = "0.8.0", features = ["embedded-fdb-include"], optional = true } rusqlite = { version = "0.30.0", features = ["bundled"], optional = true } rust-s3 = { version = "0.33.0", default-features = false, features = ["tokio-rustls-tls"], optional = true }