use rhai::plugin::{
mem, Dynamic, FnAccess, FnNamespace, ImmutableString, NativeCallContext, PluginFunction,
RhaiResult, TypeId,
};
use rhai::Module;
use std::str::FromStr;
#[derive(Debug, Clone, strum::AsRefStr)]
#[strum(serialize_all = "lowercase")]
pub enum Object {
Ip4(std::net::Ipv4Addr),
Ip6(std::net::Ipv6Addr),
Rg4(iprange::IpRange<ipnet::Ipv4Net>),
Rg6(iprange::IpRange<ipnet::Ipv6Net>),
Address(vsmtp_common::Address),
Fqdn(String),
Regex(regex::Regex),
Identifier(String),
Code(vsmtp_common::Reply),
}
pub type SharedObject = rhai::Shared<Object>;
macro_rules! new_object_from_str {
($value:expr, $type:ty, $object_type:expr) => {
<$type as std::str::FromStr>::from_str($value)
.map(|value| $object_type(value))
.map_err(|err| anyhow::anyhow!("{err}"))
};
}
impl Object {
fn from_str(s: &str, value: &str) -> anyhow::Result<Self> {
match s {
"ip4" => Self::new_ip4(value),
"ip6" => Self::new_ip6(value),
"rg4" => Self::new_rg4(value),
"rg6" => Self::new_rg6(value),
"address" => Self::new_address(value),
"fqdn" => Self::new_fqdn(value),
"regex" => Self::new_regex(value),
"identifier" => Ok(Self::new_identifier(value)),
_ => Err(anyhow::anyhow!("invalid object type: {}", s)),
}
}
pub fn new_ip4(ip: impl AsRef<str>) -> anyhow::Result<Object> {
new_object_from_str!(ip.as_ref(), std::net::Ipv4Addr, Object::Ip4)
}
pub fn new_ip6(ip: impl AsRef<str>) -> anyhow::Result<Object> {
new_object_from_str!(ip.as_ref(), std::net::Ipv6Addr, Object::Ip6)
}
pub fn new_rg4(range: impl AsRef<str>) -> anyhow::Result<Object> {
range
.as_ref()
.parse::<ipnet::Ipv4Net>()
.map(|range| Object::Rg4(std::iter::once(range).collect()))
.map_err(|error| anyhow::anyhow!("{error}"))
}
pub fn new_rg6(range: impl AsRef<str>) -> anyhow::Result<Object> {
range
.as_ref()
.parse::<ipnet::Ipv6Net>()
.map(|range| Object::Rg6(std::iter::once(range).collect()))
.map_err(|error| anyhow::anyhow!("{error}"))
}
pub fn new_address(address: impl AsRef<str>) -> anyhow::Result<Object> {
new_object_from_str!(address.as_ref(), vsmtp_common::Address, Object::Address)
}
pub fn new_fqdn(domain: impl AsRef<str>) -> anyhow::Result<Object> {
addr::parse_domain_name(domain.as_ref())
.map(|domain| Object::Fqdn(domain.to_string()))
.map_err(|error| anyhow::anyhow!("{error}"))
}
pub fn new_regex(regex: impl AsRef<str>) -> anyhow::Result<Object> {
new_object_from_str!(regex.as_ref(), regex::Regex, Object::Regex)
}
pub fn new_file(
path: impl AsRef<std::path::Path>,
content_type: impl AsRef<str>,
) -> anyhow::Result<rhai::Array> {
std::fs::read_to_string(path.as_ref())?
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| Self::from_str(content_type.as_ref(), line).map(rhai::Dynamic::from))
.collect::<anyhow::Result<rhai::Array>>()
}
pub fn new_identifier(identifier: impl Into<String>) -> Object {
Object::Identifier(identifier.into())
}
pub fn new_code(code: u16, text: impl Into<String>) -> Object {
Object::Code(vsmtp_common::Reply::new(
vsmtp_common::ReplyCode::Code { code },
text.into(),
))
}
pub fn new_code_enhanced<T>(code: u16, enhanced: T, text: T) -> Object
where
T: Into<String>,
{
Object::Code(vsmtp_common::Reply::new(
vsmtp_common::ReplyCode::Enhanced {
code,
enhanced: enhanced.into(),
},
text.into(),
))
}
}
impl Object {
#[must_use]
pub fn contains(&self, other: &Self) -> bool {
match (self, other) {
(Object::Rg4(rg4), Object::Ip4(ip4)) => rg4.contains(ip4),
(Object::Rg6(rg6), Object::Ip6(ip6)) => rg6.contains(ip6),
(Object::Regex(regex), other) => regex.find(other.as_ref()).is_some(),
(Object::Address(addr), Object::Identifier(identifier)) => {
addr.local_part() == identifier.as_str()
}
(Object::Address(addr), Object::Fqdn(fqdn)) => addr.domain() == fqdn.as_str(),
_ => false,
}
}
#[must_use]
pub fn contains_str(&self, other: &str) -> bool {
match self {
Object::Rg4(rg4) => ipnet::Ipv4Net::from_str(other)
.map(|ip4| rg4.contains(&ip4))
.unwrap_or(false),
Object::Rg6(rg6) => ipnet::Ipv6Net::from_str(other)
.map(|ip6| rg6.contains(&ip6))
.unwrap_or(false),
Object::Regex(regex) => regex.find(other).is_some(),
Object::Address(addr) => addr.local_part() == other || addr.domain() == other,
_ => false,
}
}
}
impl PartialEq for Object {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ip4(l0), Self::Ip4(r0)) => l0 == r0,
(Self::Ip6(l0), Self::Ip6(r0)) => l0 == r0,
(Self::Rg4(l0), Self::Rg4(r0)) => l0 == r0,
(Self::Rg6(l0), Self::Rg6(r0)) => l0 == r0,
(Self::Address(l0), Self::Address(r0)) => l0 == r0,
(Self::Fqdn(l0), Self::Fqdn(r0)) | (Self::Identifier(l0), Self::Identifier(r0)) => {
l0 == r0
}
(Self::Regex(r0), Self::Regex(l0)) => r0.as_str() == l0.as_str(),
(Self::Code(r0), Self::Code(l0)) => r0 == l0,
_ => false,
}
}
}
impl std::fmt::Display for Object {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Object::Ip4(ip) => write!(f, "{ip}"),
Object::Ip6(ip) => write!(f, "{ip}"),
Object::Rg4(range) => write!(f, "{range:?}"),
Object::Rg6(range) => write!(f, "{range:?}"),
Object::Address(addr) => write!(f, "{addr}"),
Object::Fqdn(fqdn) => write!(f, "{fqdn}"),
Object::Regex(regex) => write!(f, "{regex}"),
Object::Identifier(ident) => write!(f, "{ident}"),
Object::Code(reply) => write!(f, "{} {}", reply.code(), reply.text()),
}
}
}
macro_rules! new_object {
($object:expr) => {
Ok(rhai::Shared::new(
$object.map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?,
))
};
}
type RhaiResultOf<T> = Result<T, Box<rhai::EvalAltResult>>;
#[rhai::plugin::export_module]
pub mod constructors {
pub type VSLObject = crate::objects::SharedObject;
#[rhai_fn(global, return_raw)]
pub fn ip4(ip: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_ip4(ip))
}
#[rhai_fn(global, return_raw)]
pub fn ip6(ip: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_ip6(ip))
}
#[rhai_fn(global, return_raw)]
pub fn rg4(range: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_rg4(range))
}
#[rhai_fn(global, return_raw)]
pub fn rg6(range: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_rg6(range))
}
#[rhai_fn(global, return_raw)]
pub fn address(address: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_address(address))
}
#[rhai_fn(global, return_raw)]
pub fn fqdn(domain: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_fqdn(domain))
}
#[rhai_fn(global, return_raw)]
pub fn regex(regex: &str) -> RhaiResultOf<VSLObject> {
new_object!(Object::new_regex(regex))
}
#[rhai_fn(global, return_raw)]
pub fn file(path: &str, content_type: &str) -> RhaiResultOf<rhai::Array> {
Object::new_file(path, content_type)
.map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())
}
#[rhai_fn(global)]
pub fn identifier(identifier: &str) -> VSLObject {
rhai::Shared::new(Object::new_identifier(identifier))
}
#[rhai_fn(global, name = "code", return_raw)]
pub fn code(code: rhai::INT, text: &str) -> RhaiResultOf<VSLObject> {
Ok(rhai::Shared::new(Object::new_code(
u16::try_from(code).map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?,
text,
)))
}
#[rhai_fn(global, name = "code", return_raw)]
pub fn code_enhanced(code: rhai::INT, enhanced: &str, text: &str) -> RhaiResultOf<VSLObject> {
Ok(rhai::Shared::new(Object::new_code_enhanced(
u16::try_from(code).map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?,
enhanced,
text,
)))
}
}
#[rhai::plugin::export_module]
pub mod utils {
use crate::objects::constructors::VSLObject;
#[rhai_fn(global, get = "local_part", return_raw, pure)]
pub fn local_part(addr: &mut VSLObject) -> RhaiResultOf<String> {
match &**addr {
Object::Address(addr) => Ok(addr.local_part().to_string()),
other => Err(format!("cannot extract local part for {} object", other.as_ref()).into()),
}
}
#[rhai_fn(global, get = "domain", return_raw, pure)]
pub fn domain(addr: &mut VSLObject) -> RhaiResultOf<VSLObject> {
match &**addr {
Object::Address(addr) => Ok(rhai::Shared::new(Object::Fqdn(addr.domain().to_string()))),
other => Err(format!("cannot extract domain for {} object", other.as_ref()).into()),
}
}
#[rhai_fn(global, get = "local_parts", return_raw, pure)]
pub fn local_parts(container: &mut rhai::Array) -> RhaiResultOf<rhai::Array> {
container
.iter()
.map(|item| {
if item.is::<SharedObject>() {
match &*item.clone_cast::<SharedObject>() {
Object::Address(addr) => {
Ok(rhai::Dynamic::from(addr.local_part().to_string()))
}
other => Err(format!(
"cannot extract local part for non email address ({})",
other.as_ref()
)
.into()),
}
} else if item.is::<String>() {
let item = item.clone_cast::<String>();
<vsmtp_common::Address as std::str::FromStr>::from_str(item.as_str())
.map(|addr| rhai::Dynamic::from(addr.local_part().to_string()))
.map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())
} else {
Err(format!(
"cannot extract local part from a {} object.",
item.type_name()
)
.into())
}
})
.collect()
}
#[rhai_fn(global, get = "domains", return_raw, pure)]
pub fn domains(container: &mut rhai::Array) -> RhaiResultOf<rhai::Array> {
container
.iter()
.map(|item| {
if item.is::<SharedObject>() {
match &*item.clone_cast::<SharedObject>() {
Object::Address(addr) => Ok(rhai::Dynamic::from(addr.domain().to_string())),
other => Err(format!(
"cannot extract domain for non email address ({})",
other.as_ref()
)
.into()),
}
} else if item.is::<String>() {
let item = item.clone_cast::<String>();
<vsmtp_common::Address as std::str::FromStr>::from_str(item.as_str())
.map(|addr| rhai::Dynamic::from(addr.domain().to_string()))
.map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())
} else {
Err(format!("cannot extract domain from a {} object.", item.type_name()).into())
}
})
.collect()
}
#[rhai_fn(global, name = "to_string", pure)]
pub fn object_to_string(this: &mut VSLObject) -> String {
this.to_string()
}
#[rhai_fn(global, name = "to_debug", pure)]
pub fn object_to_debug(this: &mut VSLObject) -> String {
format!("{:#?}", **this)
}
}
#[rhai::plugin::export_module]
pub mod comparisons {
#[allow(clippy::needless_pass_by_value)]
#[rhai_fn(global, name = "==", pure)]
pub fn object_is_self(this: &mut SharedObject, other: SharedObject) -> bool {
**this == *other
}
#[allow(clippy::needless_pass_by_value)]
#[rhai_fn(global, name = "!=", pure)]
pub fn object_not_self(this: &mut SharedObject, other: SharedObject) -> bool {
**this != *other
}
#[rhai_fn(global, name = "==", return_raw, pure)]
pub fn object_is_string(this: &mut SharedObject, s: &str) -> RhaiResultOf<bool> {
internal_string_is_object(s, this)
}
#[rhai_fn(global, name = "!=", return_raw, pure)]
pub fn object_not_string(this: &mut SharedObject, s: &str) -> RhaiResultOf<bool> {
internal_string_is_object(s, this).map(|res| !res)
}
#[allow(clippy::needless_pass_by_value)]
#[rhai_fn(global, name = "==", return_raw)]
pub fn string_is_object(this: &str, other: SharedObject) -> RhaiResultOf<bool> {
internal_string_is_object(this, &other)
}
#[allow(clippy::needless_pass_by_value)]
#[rhai_fn(global, name = "!=", return_raw)]
pub fn string_not_object(this: &str, other: SharedObject) -> RhaiResultOf<bool> {
internal_string_is_object(this, &other).map(|res| !res)
}
#[rhai_fn(global, name = "contains", pure)]
pub fn string_in_object(this: &mut SharedObject, s: &str) -> bool {
this.contains_str(s)
}
#[allow(clippy::needless_pass_by_value)]
#[rhai_fn(global, name = "contains", pure)]
pub fn object_in_object(this: &mut SharedObject, other: SharedObject) -> bool {
this.contains(&other)
}
#[allow(clippy::needless_pass_by_value)]
#[rhai_fn(global, name = "contains", pure)]
pub fn object_in_map(map: &mut rhai::Map, object: SharedObject) -> bool {
map.contains_key(object.to_string().as_str())
}
}
fn internal_string_is_object(this: &str, other: &Object) -> RhaiResultOf<bool> {
match other {
Object::Address(addr) => Ok(this == addr.full()),
Object::Fqdn(fqdn) => Ok(this == fqdn.as_str()),
Object::Regex(re) => Ok(re.is_match(this)),
Object::Ip4(ip4) => Ok(this == ip4.to_string()),
Object::Ip6(ip6) => Ok(this == ip6.to_string()),
Object::Identifier(s) => Ok(this == s.as_str()),
_ => Err(format!("a {other} object cannot be compared to a string").into()),
}
}