config_get() expression function + moved lookup.default.[hostname|domain] to server.hostname and report.domain

This commit is contained in:
mdecimus 2025-01-12 10:53:58 +01:00
parent a4b1d5c39b
commit a6c744b09a
21 changed files with 167 additions and 107 deletions

View file

@ -14,6 +14,8 @@ use super::*;
#[derive(Clone)]
pub struct Network {
pub node_id: u64,
pub server_name: String,
pub report_domain: String,
pub security: Security,
pub contact_form: Option<ContactForm>,
pub http_response_url: IfBlock,
@ -84,10 +86,12 @@ impl Default for Network {
http_response_url: IfBlock::new::<()>(
"server.http.url",
[],
"protocol + '://' + key_get('default', 'hostname') + ':' + local_port",
"protocol + '://' + config_get('server.hostname') + ':' + local_port",
),
http_allowed_endpoint: IfBlock::new::<()>("server.http.allowed-endpoint", [], "200"),
asn_geo_lookup: AsnGeoLookupConfig::Disabled,
server_name: Default::default(),
report_domain: Default::default(),
}
}
}
@ -148,8 +152,37 @@ impl FieldOrDefault {
impl Network {
pub fn parse(config: &mut Config) -> Self {
let server_name = config
.value("server.hostname")
.map(|v| v.to_string())
.or_else(|| {
config
.value("lookup.default.hostname")
.map(|v| v.to_lowercase())
})
.unwrap_or_else(|| {
hostname::get()
.map(|v| v.to_string_lossy().to_lowercase())
.unwrap_or_else(|_| "localhost".to_string())
});
let report_domain = config
.value("report.domain")
.map(|v| v.to_lowercase())
.or_else(|| {
config
.value("lookup.default.domain")
.map(|v| v.to_lowercase())
})
.unwrap_or_else(|| {
psl::domain_str(&server_name)
.unwrap_or(server_name.as_str())
.to_string()
});
let mut network = Network {
node_id: config.property("cluster.node-id").unwrap_or_default(),
report_domain,
server_name,
security: Security::parse(config),
contact_form: ContactForm::parse(config),
asn_geo_lookup: AsnGeoLookupConfig::parse(config).unwrap_or_default(),

View file

@ -265,7 +265,7 @@ impl Scripting {
let hostname = config
.value("sieve.trusted.hostname")
.or_else(|| config.value("lookup.default.hostname"))
.or_else(|| config.value("server.hostname"))
.unwrap_or("localhost")
.to_string();
trusted_runtime.set_local_hostname(hostname.clone());
@ -327,7 +327,7 @@ impl Scripting {
IfBlock::new::<()>(
"sieve.trusted.from-addr",
[],
"'MAILER-DAEMON@' + key_get('default', 'domain')",
"'MAILER-DAEMON@' + config_get('report.domain')",
)
}),
from_name: IfBlock::try_parse(config, "sieve.trusted.from-name", &token_map)
@ -342,8 +342,8 @@ impl Scripting {
"sieve.trusted.sign",
[],
concat!(
"['rsa-' + key_get('default', 'domain'), ",
"'ed25519-' + key_get('default', 'domain')]"
"['rsa-' + config_get('report.domain'), ",
"'ed25519-' + config_get('report.domain')]"
),
)
},
@ -363,7 +363,7 @@ impl Default for Scripting {
from_addr: IfBlock::new::<()>(
"sieve.trusted.from-addr",
[],
"'MAILER-DAEMON@' + key_get('default', 'domain')",
"'MAILER-DAEMON@' + config_get('report.domain')",
),
from_name: IfBlock::new::<()>("sieve.trusted.from-name", [], "'Mailer Daemon'"),
return_path: IfBlock::empty("sieve.trusted.return-path"),
@ -371,8 +371,8 @@ impl Default for Scripting {
"sieve.trusted.sign",
[],
concat!(
"['rsa-' + key_get('default', 'domain'), ",
"'ed25519-' + key_get('default', 'domain')]"
"['rsa-' + config_get('report.domain'), ",
"'ed25519-' + config_get('report.domain')]"
),
),
untrusted_scripts: AHashMap::new(),

View file

@ -109,7 +109,7 @@ impl Default for MailAuthConfig {
seal: IfBlock::new::<()>(
"auth.arc.seal",
[],
"'rsa-' + key_get('default', 'domain')",
"'rsa-' + config_get('report.domain')",
),
},
spf: SpfAuthConfig {

View file

@ -134,11 +134,7 @@ impl Default for QueueConfig {
),
notify: IfBlock::new::<()>("queue.schedule.notify", [], "[1d, 3d]"),
expire: IfBlock::new::<()>("queue.schedule.expire", [], "5d"),
hostname: IfBlock::new::<()>(
"queue.outbound.hostname",
[],
"key_get('default', 'hostname')",
),
hostname: IfBlock::new::<()>("queue.outbound.hostname", [], "config_get('server.hostname')"),
next_hop: IfBlock::new::<()>(
"queue.outbound.next-hop",
#[cfg(not(feature = "test_mode"))]
@ -187,12 +183,12 @@ impl Default for QueueConfig {
address: IfBlock::new::<()>(
"report.dsn.from-address",
[],
"'MAILER-DAEMON@' + key_get('default', 'domain')",
"'MAILER-DAEMON@' + config_get('report.domain')",
),
sign: IfBlock::new::<()>(
"report.dsn.sign",
[],
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
"['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]",
),
},
timeout: QueueOutboundTimeout {

View file

@ -79,7 +79,7 @@ impl ReportConfig {
&TokenMap::default().with_variables(RCPT_DOMAIN_VARS),
)
.unwrap_or_else(|| {
IfBlock::new::<()>("report.submitter", [], "key_get('default', 'hostname')")
IfBlock::new::<()>("report.submitter", [], "config_get('server.hostname')")
}),
analysis: ReportAnalysis {
addresses: config
@ -118,7 +118,7 @@ impl Report {
address: IfBlock::new::<()>(
format!("report.{id}.from-address"),
[],
format!("'noreply-{id}@' + key_get('default', 'domain')"),
format!("'noreply-{id}@' + config_get('report.domain')"),
),
subject: IfBlock::new::<()>(
format!("report.{id}.subject"),
@ -131,7 +131,7 @@ impl Report {
sign: IfBlock::new::<()>(
format!("report.{id}.sign"),
[],
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
"['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]",
),
send: IfBlock::new::<()>(format!("report.{id}.send"), [], "[1, 1d]"),
};
@ -164,12 +164,12 @@ impl AggregateReport {
address: IfBlock::new::<()>(
format!("report.{id}.aggregate.from-address"),
[],
format!("'noreply-{id}@' + key_get('default', 'domain')"),
format!("'noreply-{id}@' + config_get('report.domain')"),
),
org_name: IfBlock::new::<()>(
format!("report.{id}.aggregate.org-name"),
[],
"key_get('default', 'domain')",
"config_get('report.domain')",
),
contact_info: IfBlock::empty(format!("report.{id}.aggregate.contact-info")),
send: IfBlock::new::<AggregateFrequency>(
@ -180,7 +180,7 @@ impl AggregateReport {
sign: IfBlock::new::<()>(
format!("report.{id}.aggregate.sign"),
[],
"['rsa-' + key_get('default', 'domain'), 'ed25519-' + key_get('default', 'domain')]",
"['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]",
),
max_size: IfBlock::new::<()>(format!("report.{id}.aggregate.max-size"), [], "26214400"),
};

View file

@ -702,13 +702,13 @@ impl Default for SessionConfig {
hostname: IfBlock::new::<()>(
"server.connect.hostname",
[],
"key_get('default', 'hostname')",
"config_get('server.hostname')",
),
script: IfBlock::empty("session.connect.script"),
greeting: IfBlock::new::<()>(
"session.connect.greeting",
[],
"key_get('default', 'hostname') + ' Stalwart ESMTP at your service'",
"config_get('server.hostname') + ' Stalwart ESMTP at your service'",
),
},
ehlo: Ehlo {

View file

@ -37,7 +37,9 @@ impl Enterprise {
stores: &Stores,
data: &Store,
) -> Option<Self> {
let server_hostname = config.value("lookup.default.hostname")?;
let server_hostname = config
.value("server.hostname")
.or_else(|| config.value("lookup.default.hostname"))?;
let mut update_license = None;
let license_result = match (

View file

@ -14,12 +14,12 @@ use crate::Server;
use super::{
functions::{ResolveVariable, FUNCTIONS},
if_block::IfBlock,
BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator, Variable,
BinaryOperator, Constant, Expression, ExpressionItem, Setting, UnaryOperator, Variable,
};
impl Server {
pub async fn eval_if<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>(
&self,
&'x self,
if_block: &'x IfBlock,
resolver: &'x V,
session_id: u64,
@ -81,7 +81,7 @@ impl Server {
}
pub async fn eval_expr<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>(
&self,
&'x self,
expr: &'x Expression,
resolver: &'x V,
expr_id: &str,
@ -137,15 +137,15 @@ impl Server {
}
}
struct EvalContext<'x, 'y, V: ResolveVariable, T, C> {
struct EvalContext<'x, V: ResolveVariable, T, C> {
resolver: &'x V,
core: &'y Server,
core: &'x Server,
expr: &'x T,
captures: C,
session_id: u64,
}
impl<'x, V: ResolveVariable> EvalContext<'x, '_, V, IfBlock, Vec<String>> {
impl<'x, V: ResolveVariable> EvalContext<'x, V, IfBlock, Vec<String>> {
async fn eval(&mut self) -> trc::Result<Variable<'x>> {
for if_then in &self.expr.if_then {
if (EvalContext {
@ -183,7 +183,7 @@ impl<'x, V: ResolveVariable> EvalContext<'x, '_, V, IfBlock, Vec<String>> {
}
}
impl<'x, V: ResolveVariable> EvalContext<'x, '_, V, Expression, &mut Vec<String>> {
impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec<String>> {
async fn eval(&mut self) -> trc::Result<Variable<'x>> {
let mut stack = Vec::new();
let mut exprs = self.expr.items.iter();
@ -208,6 +208,25 @@ impl<'x, V: ResolveVariable> EvalContext<'x, '_, V, Expression, &mut Vec<String>
.to_string(),
)));
}
ExpressionItem::Setting(setting) => match setting {
Setting::Hostname => {
stack.push(self.core.core.network.server_name.as_str().into())
}
Setting::ReportDomain => {
stack.push(self.core.core.network.report_domain.as_str().into())
}
Setting::NodeId => stack.push(self.core.core.network.node_id.into()),
Setting::Other(key) => stack.push(
self.core
.core
.storage
.config
.get(key)
.await?
.unwrap_or_default()
.into(),
),
},
ExpressionItem::UnaryOperator(op) => {
let value = stack.pop().unwrap_or_default();
stack.push(match op {

View file

@ -88,6 +88,7 @@ pub struct Expression {
pub enum ExpressionItem {
Variable(u32),
Global(String),
Setting(Setting),
Capture(u32),
Constant(Constant),
BinaryOperator(BinaryOperator),
@ -200,6 +201,7 @@ pub enum Token {
num_args: u32,
},
Constant(Constant),
Setting(Setting),
Regex(Regex),
BinaryOperator(BinaryOperator),
UnaryOperator(UnaryOperator),
@ -210,6 +212,25 @@ pub enum Token {
Comma,
}
#[derive(Debug, Clone)]
pub enum Setting {
Hostname,
ReportDomain,
NodeId,
Other(String),
}
impl From<String> for Setting {
fn from(value: String) -> Self {
match value.as_str() {
"server.hostname" => Setting::Hostname,
"report.domain" => Setting::ReportDomain,
"cluster.node-id" => Setting::NodeId,
_ => Setting::Other(value),
}
}
}
impl From<usize> for Variable<'_> {
fn from(value: usize) -> Self {
Variable::Integer(value as i64)

View file

@ -109,6 +109,16 @@ impl<'x> ExpressionParser<'x> {
self.output.push(ExpressionItem::Regex(regex.clone()));
self.operator_stack.pop();
}
Some((Token::Setting(setting), _)) => {
if self.arg_count.pop().unwrap() != 0 {
return Err(
"Expression function \"config_get\" expected 1 argument"
.to_string(),
);
}
self.output.push(ExpressionItem::Setting(setting.clone()));
self.operator_stack.pop();
}
_ => {}
}
@ -156,16 +166,10 @@ impl<'x> ExpressionParser<'x> {
self.operator_stack
.push((Token::BinaryOperator(bop), jmp_pos));
}
Token::Function { id, name, num_args } => {
token @ (Token::Function { .. } | Token::Regex(_) | Token::Setting(_)) => {
self.inc_arg_count();
self.arg_count.push(0);
self.operator_stack
.push((Token::Function { id, name, num_args }, None))
}
Token::Regex(regex) => {
self.inc_arg_count();
self.arg_count.push(0);
self.operator_stack.push((Token::Regex(regex), None))
self.operator_stack.push((token, None))
}
Token::OpenBracket => {
// Array functions

View file

@ -100,6 +100,13 @@ impl<'x> Tokenizer<'x> {
self.buf.clear();
self.find_char(b",")?;
(Token::Regex(regex).into(), b'(')
} else if ch == b'(' && self.buf.eq(b"config_get") {
// Parse setting
let stop_ch = self.find_char(b"\"'")?;
let setting_str = self.parse_string(stop_ch)?;
self.has_alpha = false;
self.buf.clear();
(Token::Setting(Setting::from(setting_str)).into(), b'(')
} else if !self.buf.is_empty() {
self.is_start = false;
(self.parse_buf()?.into(), ch)

View file

@ -195,18 +195,6 @@ impl BootManager {
StoreOp::None => {
// Add hostname lookup if missing
let mut insert_keys = Vec::new();
if config
.value("lookup.default.hostname")
.filter(|v| !v.is_empty())
.is_none()
{
insert_keys.push(ConfigKey::from((
"lookup.default.hostname",
hostname::get()
.map(|v| v.to_string_lossy().into_owned())
.unwrap_or_else(|_| "localhost".to_string()),
)));
}
// Generate an OAuth key if missing
if config

View file

@ -423,7 +423,7 @@ impl ConfigManager {
required_semver = value.as_str().try_into().unwrap_or_default();
} else if key.starts_with("spam-filter.")
|| key.starts_with("http-lookup.")
|| (key.starts_with("lookup.") && !key.starts_with("lookup.default."))
|| key.starts_with("lookup.")
|| key.starts_with("asn.")
{
external.keys.push(ConfigKey::from((key, value)));
@ -537,7 +537,6 @@ impl Patterns {
Pattern::Include(MatchType::Equal("storage.lookup".to_string())),
Pattern::Include(MatchType::Equal("storage.fts".to_string())),
Pattern::Include(MatchType::Equal("storage.directory".to_string())),
Pattern::Include(MatchType::Equal("lookup.default.hostname".to_string())),
Pattern::Include(MatchType::Equal("enterprise.license-key".to_string())),
];
}

View file

@ -40,7 +40,7 @@ impl SmtpDirectory {
is_lmtp,
credentials: None,
local_host: config
.value("lookup.default.hostname")
.value("server.hostname")
.unwrap_or("[127.0.0.1]")
.to_string(),
say_ehlo: false,

View file

@ -180,17 +180,7 @@ impl Autoconfig for Server {
})?;
// Obtain server name
let server_name = self
.core
.storage
.config
.get("lookup.default.hostname")
.await?
.ok_or_else(|| {
trc::EventType::Config(trc::ConfigEvent::BuildError)
.caused_by(trc::location!())
.details("Server name not configured")
})?;
let server_name = self.core.network.server_name.to_string();
// Find the account name by e-mail address
let mut account_name = emailaddress.to_string();

View file

@ -77,13 +77,7 @@ impl DnsManagement for Server {
async fn build_dns_records(&self, domain_name: &str) -> trc::Result<Vec<DnsRecord>> {
// Obtain server name
let server_name = self
.core
.storage
.config
.get("lookup.default.hostname")
.await?
.unwrap_or_else(|| "localhost".to_string());
let server_name = &self.core.network.server_name;
let mut records = Vec::new();
// Obtain DKIM keys

View file

@ -146,6 +146,21 @@ impl PrincipalManager for Server {
}
}
// Set default report domain if missing
let report_domain = if principal.typ() == Type::Domain
&& self
.core
.storage
.config
.get("report.domain")
.await
.is_ok_and(|v| v.is_none())
{
principal.name().to_lowercase().into()
} else {
None
};
// Create principal
let result = self
.core
@ -154,6 +169,19 @@ impl PrincipalManager for Server {
.create_principal(principal, tenant_id, Some(&access_token.permissions))
.await?;
// Set report domain
if let Some(report_domain) = report_domain {
if let Err(err) = self
.core
.storage
.config
.set([("report.domain", report_domain)], true)
.await
{
trc::error!(err.details("Failed to set report domain"));
}
}
Ok(JsonResponse::new(json!({
"data": result,
}))

View file

@ -132,14 +132,7 @@ impl ManageSpamHandler for Server {
let ehlo_domain = request.ehlo_domain.to_lowercase();
let mail_from = request.env_from.to_lowercase();
let mail_from_domain = mail_from.rsplit_once('@').map(|(_, domain)| domain);
let local_host = self
.core
.storage
.config
.get("lookup.default.hostname")
.await
.unwrap_or_default()
.unwrap_or_else(|| "local.host".to_string());
let local_host = &self.core.network.server_name;
let spf_ehlo_result =
self.core
@ -147,7 +140,7 @@ impl ManageSpamHandler for Server {
.resolvers
.dns
.verify_spf(self.inner.cache.build_auth_parameters(
SpfParameters::verify_ehlo(remote_ip, &ehlo_domain, &local_host),
SpfParameters::verify_ehlo(remote_ip, &ehlo_domain, local_host),
))
.await;
@ -168,7 +161,7 @@ impl ManageSpamHandler for Server {
remote_ip,
mail_from_domain,
&ehlo_domain,
&local_host,
local_host,
&mail_from,
)))
.await
@ -181,7 +174,7 @@ impl ManageSpamHandler for Server {
remote_ip,
&ehlo_domain,
&ehlo_domain,
&local_host,
local_host,
&format!("postmaster@{ehlo_domain}"),
)))
.await

View file

@ -318,14 +318,7 @@ async fn delivery_troubleshoot(
(domain_or_email, None)
};
let local_host = server
.core
.storage
.config
.get("lookup.default.hostname")
.await
.unwrap_or_default()
.unwrap_or_else(|| "local.host".to_string());
let local_host = &server.core.network.server_name;
tx.send(DeliveryStage::MxLookupStart {
domain: domain.to_string(),
@ -897,14 +890,7 @@ async fn dmarc_troubleshoot(
let mail_from = request.mail_from.to_lowercase();
let mail_from_domain = mail_from.rsplit_once('@').map(|(_, domain)| domain);
let local_host = server
.core
.storage
.config
.get("lookup.default.hostname")
.await
.unwrap_or_default()
.unwrap_or_else(|| "local.host".to_string());
let local_host = &server.core.network.server_name;
let now = Instant::now();
let ehlo_spf_output = server
@ -919,7 +905,7 @@ async fn dmarc_troubleshoot(
.build_auth_parameters(SpfParameters::verify_ehlo(
remote_ip,
&ehlo_domain,
&local_host,
local_host,
)),
)
.await;
@ -941,7 +927,7 @@ async fn dmarc_troubleshoot(
remote_ip,
mail_from_domain,
&ehlo_domain,
&local_host,
local_host,
&mail_from,
)))
.await
@ -955,7 +941,7 @@ async fn dmarc_troubleshoot(
remote_ip,
&ehlo_domain,
&ehlo_domain,
&local_host,
local_host,
&format!("postmaster@{ehlo_domain}"),
)))
.await

View file

@ -56,7 +56,7 @@ use crate::{
};
const SERVER: &str = r#"
[lookup.default]
[server]
hostname = "imap.example.org"
[server.listener.imap]

View file

@ -29,15 +29,15 @@ const CONFIG: &str = r#"
[session.rcpt]
relay = true
[lookup.default]
domain = "example.org"
[server]
hostname = "mx.example.org"
[report]
submitter = "'mx.example.org'"
[report.dmarc.aggregate]
from-name = "'DMARC Report'"
from-address = "'reports@' + key_get('default', 'domain')"
from-address = "'reports@' + config_get('report.domain')"
org-name = "'Foobar, Inc.'"
contact-info = "'https://foobar.org/contact'"
send = "daily"