use crate::{
api::{
EngineResult, {Context, Server},
},
error::RuntimeError,
};
use rhai::plugin::{
mem, Dynamic, FnAccess, FnNamespace, Module, NativeCallContext, PluginFunction, RhaiResult,
TypeId,
};
use vsmtp_auth::viaspf;
use vsmtp_common::ClientName;
const AUTH_HEADER: &str = "Authentication-Results";
const SPF_HEADER: &str = "Received-SPF";
pub use spf::*;
#[derive(Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
enum Policy {
#[default]
Strict,
Soft,
}
#[derive(Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
enum Headers {
Spf,
Auth,
#[default]
Both,
None,
}
#[derive(Default, serde::Deserialize)]
struct SpfParameters {
#[serde(default)]
header: Headers,
#[serde(default)]
policy: Policy,
}
#[rhai::plugin::export_module]
mod spf {
use crate::api::{message::Impl, state};
use vsmtp_common::status::Status;
use crate::get_global;
#[rhai_fn(name = "check", return_raw)]
pub fn check_no_params(ncc: NativeCallContext) -> EngineResult<Status> {
super::spf::check_with_params(ncc, rhai::Map::default())
}
#[doc(hidden)]
#[rhai_fn(name = "check", return_raw)]
pub fn check_with_params(ncc: NativeCallContext, params: rhai::Map) -> EngineResult<Status> {
let params = rhai::serde::from_dynamic::<SpfParameters>(¶ms.into())?;
let ctx = get_global!(ncc, ctx);
let srv = get_global!(ncc, srv);
let query = super::check(&ctx, &srv)?;
let msg = get_global!(ncc, msg);
let (hostname, sender, client_ip) = {
let ctx = vsl_guard_ok!(ctx.read());
(
vsmtp_plugin_vsl::unix::hostname()?,
vsl_generic_ok!(ctx.reverse_path()).clone(),
ctx.client_addr().ip().to_string(),
)
};
match params.header {
Headers::Spf => Impl::prepend_header(
&msg,
SPF_HEADER,
&super::spf_header(
&query,
&hostname,
sender.as_ref().map_or("null", |sender| sender.full()),
&client_ip,
),
),
Headers::Auth => Impl::prepend_header(
&msg,
AUTH_HEADER,
&super::auth_header(
&query,
&hostname,
sender.as_ref().map_or("null", |sender| sender.full()),
&client_ip,
),
),
Headers::Both => {
Impl::prepend_header(
&msg,
AUTH_HEADER,
&super::auth_header(
&query,
&hostname,
sender.as_ref().map_or("null", |sender| sender.full()),
&client_ip,
),
);
Impl::prepend_header(
&msg,
SPF_HEADER,
&super::spf_header(
&query,
&hostname,
sender.as_ref().map_or("null", |sender| sender.full()),
&client_ip,
),
);
}
Headers::None => {}
};
match params.policy {
Policy::Strict => {
Ok(match query.result.as_str() {
"pass" => state::next(),
"temperror" | "permerror" => {
state::deny_with_code(&mut crate::api::code::c550_7_24())?
}
_ => state::deny_with_code(&mut crate::api::code::c550_7_23())?,
})
}
Policy::Soft => {
Ok(match query.result.as_str() {
"pass" | "softfail" => state::next(),
"temperror" | "permerror" => {
state::deny_with_code(&mut crate::api::code::c550_7_24())?
}
_ => state::deny_with_code(&mut crate::api::code::c550_7_23())?,
})
}
}
}
#[rhai_fn(name = "check_raw", return_raw)]
pub fn check_raw(ncc: NativeCallContext) -> EngineResult<rhai::Map> {
let ctx = get_global!(ncc, ctx);
let srv = get_global!(ncc, srv);
super::check(&ctx, &srv).map(|spf| result_to_map(&spf))
}
}
pub fn check(ctx: &Context, srv: &Server) -> EngineResult<vsmtp_auth::spf::Result> {
let (spf_sender, ip) = {
let ctx = vsl_guard_ok!(ctx.read());
let mail_from = ctx.reverse_path().map_err(Into::<RuntimeError>::into)?;
let spf_sender = match mail_from {
Some(mail_from) => vsl_generic_ok!(viaspf::Sender::from_address(mail_from.full())),
None => {
let client_name = ctx.client_name().map_err(Into::<RuntimeError>::into)?;
match client_name {
ClientName::Domain(domain) => {
vsl_generic_ok!(viaspf::Sender::from_domain(&domain.to_string()))
}
ClientName::Ip4(_) | ClientName::Ip6(_) => {
return Ok(vsmtp_auth::spf::Result {
result: "fail".to_owned(),
details: vsmtp_auth::spf::Details::Problem(
"HELO identity is invalid".to_lowercase(),
),
})
}
}
}
};
(spf_sender, ctx.client_addr().ip())
};
let resolver = srv.resolvers.get_resolver_root();
let spf_result = block_on!(vsmtp_auth::spf::evaluate(&resolver, ip, &spf_sender));
vsl_guard_ok!(ctx.write())
.set_spf(spf_result.clone())
.map_err(Into::<RuntimeError>::into)?;
Ok(spf_result)
}
#[must_use]
pub fn key_value_list(
spf: &vsmtp_auth::spf::Result,
hostname: &str,
sender: &str,
client_ip: &str,
) -> String {
format!(
r#"receiver={};
client-ip={};
envelope_from={};
identity=mailfrom;
{}`
"#,
hostname,
client_ip,
sender,
match &spf.details {
vsmtp_auth::spf::Details::Mechanism(mechanism) => format!("mechanism={mechanism};"),
vsmtp_auth::spf::Details::Problem(problem) => format!("problem={problem};"),
},
)
}
fn spf_header(
spf: &vsmtp_auth::spf::Result,
hostname: &str,
sender: &str,
client_ip: &str,
) -> String {
format!(
"{} {}",
spf.result,
key_value_list(spf, hostname, sender, client_ip)
)
}
fn auth_header(
spf: &vsmtp_auth::spf::Result,
hostname: &str,
sender: &str,
client_ip: &str,
) -> String {
format!(
r#"{}; spf={}
reason="{}"
smtp.mailfrom={}"#,
hostname,
spf.result,
key_value_list(spf, hostname, sender, client_ip),
sender
)
}
fn result_to_map(spf: &vsmtp_auth::spf::Result) -> rhai::Map {
rhai::Map::from_iter([
("result".into(), rhai::Dynamic::from(spf.result.clone())),
match &spf.details {
vsmtp_auth::spf::Details::Mechanism(mechanism) => {
("mechanism".into(), mechanism.into())
}
vsmtp_auth::spf::Details::Problem(error) => ("problem".into(), error.into()),
},
])
}