/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use self::resolver::Policy; use super::*; use crate::expr::{ Variable, if_block::{BootstrapExprExt, IfBlock}, }; use ahash::AHashSet; use hyper::HeaderMap; use registry::schema::{ enums::{self, ExpressionConstant, MtaStage}, prelude::ObjectType, structs::{ MtaExtensions, MtaHook, MtaInboundSession, MtaMilter, MtaStageAuth, MtaStageConnect, MtaStageData, MtaStageEhlo, MtaStageMail, MtaStageRcpt, }, }; use smtp_proto::*; use std::{ net::{SocketAddr, ToSocketAddrs}, str::FromStr, time::Duration, }; #[derive(Clone)] pub struct SessionConfig { pub timeout: IfBlock, pub duration: IfBlock, pub transfer_limit: IfBlock, pub connect: Connect, pub ehlo: Ehlo, pub auth: Auth, pub mail: Mail, pub rcpt: Rcpt, pub data: Data, pub extensions: Extensions, pub mta_sts_policy: Option, pub milters: Vec, pub hooks: Vec, } #[derive(Clone)] pub struct Connect { pub hostname: IfBlock, pub script: IfBlock, pub greeting: IfBlock, } #[derive(Clone)] pub struct Ehlo { pub script: IfBlock, pub require: IfBlock, pub reject_non_fqdn: IfBlock, } #[derive(Clone)] pub struct Extensions { pub pipelining: IfBlock, pub chunking: IfBlock, pub requiretls: IfBlock, pub dsn: IfBlock, pub vrfy: IfBlock, pub expn: IfBlock, pub no_soliciting: IfBlock, pub future_release: IfBlock, pub deliver_by: IfBlock, pub mt_priority: IfBlock, } #[derive(Clone)] pub struct Auth { pub mechanisms: IfBlock, pub require: IfBlock, pub must_match_sender: IfBlock, pub errors_max: IfBlock, pub errors_wait: IfBlock, } #[derive(Clone)] pub struct Mail { pub script: IfBlock, pub rewrite: IfBlock, pub is_allowed: IfBlock, } #[derive(Clone)] pub struct Rcpt { pub script: IfBlock, pub relay: IfBlock, pub rewrite: IfBlock, pub errors_max: IfBlock, pub errors_wait: IfBlock, pub max_recipients: IfBlock, } #[derive(Debug, Default, Clone)] pub enum AddressMapping { Enable, Custom(IfBlock), #[default] Disable, } #[derive(Clone)] pub struct Data { pub script: IfBlock, pub spam_filter: IfBlock, pub max_messages: IfBlock, pub max_message_size: IfBlock, pub max_received_headers: IfBlock, pub add_received: IfBlock, pub add_received_spf: IfBlock, pub add_return_path: IfBlock, pub add_auth_results: IfBlock, pub add_message_id: IfBlock, pub add_date: IfBlock, pub add_delivered_to: bool, } #[derive(Clone)] pub struct Milter { pub enable: IfBlock, pub id: ObjectId, pub addrs: Vec, pub hostname: String, pub port: u16, pub timeout_connect: Duration, pub timeout_command: Duration, pub timeout_data: Duration, pub tls: bool, pub tls_allow_invalid_certs: bool, pub tempfail_on_error: bool, pub max_frame_len: usize, pub protocol_version: MilterVersion, pub flags_actions: Option, pub flags_protocol: Option, pub run_on_stage: AHashSet, } #[derive(Clone, Copy)] pub enum MilterVersion { V2, V6, } #[derive(Clone)] pub struct MTAHook { pub enable: IfBlock, pub id: ObjectId, pub url: String, pub timeout: Duration, pub headers: HeaderMap, pub tls_allow_invalid_certs: bool, pub tempfail_on_error: bool, pub run_on_stage: AHashSet, pub max_response_size: usize, pub client: reqwest::Client, } #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum Stage { Connect, Ehlo, Auth, Mail, Rcpt, Data, } impl SessionConfig { pub async fn parse(bp: &mut Bootstrap) -> Self { let session = bp.setting_infallible::().await; let connect = bp.setting_infallible::().await; let auth = bp.setting_infallible::().await; let ehlo = bp.setting_infallible::().await; let mail = bp.setting_infallible::().await; let rcpt = bp.setting_infallible::().await; let data = bp.setting_infallible::().await; let ext = bp.setting_infallible::().await; let mut hooks = Vec::new(); for hook in bp.list_infallible::().await { let id = hook.id; let hook = hook.object; let enable = bp.compile_expr(id, &hook.ctx_enable()); let headers = match hook .http_auth .build_headers(hook.http_headers, "application/json".into()) .await { Ok(headers) => headers, Err(err) => { bp.build_error(id, format!("Unable to build HTTP headers: {}", err)); continue; } }; hooks.push(MTAHook { enable, id, url: hook.url, timeout: hook.timeout.into_inner(), headers, tls_allow_invalid_certs: hook.allow_invalid_certs, tempfail_on_error: hook.temp_fail_on_error, run_on_stage: hook.stages.into_iter().map(Stage::from).collect(), max_response_size: hook.max_response_size as usize, client: utils::http::http_client_builder(hook.allow_invalid_certs) .build() .unwrap_or_default(), }); } SessionConfig { timeout: bp.compile_expr( ObjectType::MtaInboundSession.singleton(), &session.ctx_timeout(), ), duration: bp.compile_expr( ObjectType::MtaInboundSession.singleton(), &session.ctx_max_duration(), ), transfer_limit: bp.compile_expr( ObjectType::MtaInboundSession.singleton(), &session.ctx_transfer_limit(), ), connect: Connect { hostname: bp.compile_expr( ObjectType::MtaStageConnect.singleton(), &connect.ctx_hostname(), ), script: bp.compile_expr( ObjectType::MtaStageConnect.singleton(), &connect.ctx_script(), ), greeting: bp.compile_expr( ObjectType::MtaStageConnect.singleton(), &connect.ctx_smtp_greeting(), ), }, ehlo: Ehlo { script: bp.compile_expr(ObjectType::MtaStageEhlo.singleton(), &ehlo.ctx_script()), require: bp.compile_expr(ObjectType::MtaStageEhlo.singleton(), &ehlo.ctx_require()), reject_non_fqdn: bp.compile_expr( ObjectType::MtaStageEhlo.singleton(), &ehlo.ctx_reject_non_fqdn(), ), }, auth: Auth { mechanisms: bp.compile_expr( ObjectType::MtaStageAuth.singleton(), &auth.ctx_sasl_mechanisms(), ), require: bp.compile_expr(ObjectType::MtaStageAuth.singleton(), &auth.ctx_require()), must_match_sender: bp.compile_expr( ObjectType::MtaStageAuth.singleton(), &auth.ctx_must_match_sender(), ), errors_max: bp.compile_expr( ObjectType::MtaStageAuth.singleton(), &auth.ctx_max_failures(), ), errors_wait: bp.compile_expr( ObjectType::MtaStageAuth.singleton(), &auth.ctx_wait_on_fail(), ), }, mail: Mail { script: bp.compile_expr(ObjectType::MtaStageMail.singleton(), &mail.ctx_script()), rewrite: bp.compile_expr(ObjectType::MtaStageMail.singleton(), &mail.ctx_rewrite()), is_allowed: bp.compile_expr( ObjectType::MtaStageMail.singleton(), &mail.ctx_is_sender_allowed(), ), }, rcpt: Rcpt { script: bp.compile_expr(ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_script()), relay: bp.compile_expr( ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_allow_relaying(), ), rewrite: bp.compile_expr(ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_rewrite()), errors_max: bp.compile_expr( ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_max_failures(), ), errors_wait: bp.compile_expr( ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_wait_on_fail(), ), max_recipients: bp.compile_expr( ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_max_recipients(), ), }, data: Data { script: bp.compile_expr(ObjectType::MtaStageData.singleton(), &data.ctx_script()), spam_filter: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_enable_spam_filter(), ), max_messages: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_max_messages(), ), max_message_size: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_max_message_size(), ), max_received_headers: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_max_received_headers(), ), add_received: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_add_received_header(), ), add_received_spf: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_add_received_spf_header(), ), add_return_path: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_add_return_path_header(), ), add_auth_results: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_add_auth_results_header(), ), add_message_id: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_add_message_id_header(), ), add_date: bp.compile_expr( ObjectType::MtaStageData.singleton(), &data.ctx_add_date_header(), ), add_delivered_to: data.add_delivered_to_header, }, extensions: Extensions { pipelining: bp .compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_pipelining()), chunking: bp .compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_chunking()), requiretls: bp.compile_expr( ObjectType::MtaExtensions.singleton(), &ext.ctx_require_tls(), ), dsn: bp.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_dsn()), vrfy: bp.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_vrfy()), expn: bp.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_expn()), no_soliciting: bp.compile_expr( ObjectType::MtaExtensions.singleton(), &ext.ctx_no_soliciting(), ), future_release: bp.compile_expr( ObjectType::MtaExtensions.singleton(), &ext.ctx_future_release(), ), deliver_by: bp .compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_deliver_by()), mt_priority: bp.compile_expr( ObjectType::MtaExtensions.singleton(), &ext.ctx_mt_priority(), ), }, mta_sts_policy: Policy::try_parse(bp).await, milters: bp .list_infallible::() .await .into_iter() .filter_map(|milter| { let id = milter.id; let milter = milter.object; Some(Milter { enable: bp.compile_expr(id, &milter.ctx_enable()), id, addrs: format!("{}:{}", milter.hostname, milter.port) .to_socket_addrs() .map_err(|err| { bp.build_error( id, format!( "Unable to resolve milter hostname {}: {}", milter.hostname, err ), ) }) .ok()? .collect(), hostname: milter.hostname, port: milter.port as u16, timeout_connect: milter.timeout_connect.into_inner(), timeout_command: milter.timeout_command.into_inner(), timeout_data: milter.timeout_data.into_inner(), tls: milter.use_tls, tls_allow_invalid_certs: milter.allow_invalid_certs, tempfail_on_error: milter.temp_fail_on_error, max_frame_len: milter.max_response_size as usize, protocol_version: match milter.protocol_version { enums::MilterVersion::V2 => MilterVersion::V2, enums::MilterVersion::V6 => MilterVersion::V6, }, flags_actions: milter.flags_action.map(|v| v as u32), flags_protocol: milter.flags_protocol.map(|v| v as u32), run_on_stage: milter.stages.into_iter().map(Stage::from).collect(), }) }) .collect(), hooks, } } } #[derive(Default)] pub struct Mechanism(u64); impl FromStr for Mechanism { type Err = String; fn from_str(value: &str) -> Result { Ok(Mechanism(match value.to_ascii_uppercase().as_str() { "LOGIN" => AUTH_LOGIN, "PLAIN" => AUTH_PLAIN, "XOAUTH2" => AUTH_XOAUTH2, "OAUTHBEARER" => AUTH_OAUTHBEARER, /*"SCRAM-SHA-256-PLUS" => AUTH_SCRAM_SHA_256_PLUS, "SCRAM-SHA-256" => AUTH_SCRAM_SHA_256, "SCRAM-SHA-1-PLUS" => AUTH_SCRAM_SHA_1_PLUS, "SCRAM-SHA-1" => AUTH_SCRAM_SHA_1, "XOAUTH" => AUTH_XOAUTH, "9798-M-DSA-SHA1" => AUTH_9798_M_DSA_SHA1, "9798-M-ECDSA-SHA1" => AUTH_9798_M_ECDSA_SHA1, "9798-M-RSA-SHA1-ENC" => AUTH_9798_M_RSA_SHA1_ENC, "9798-U-DSA-SHA1" => AUTH_9798_U_DSA_SHA1, "9798-U-ECDSA-SHA1" => AUTH_9798_U_ECDSA_SHA1, "9798-U-RSA-SHA1-ENC" => AUTH_9798_U_RSA_SHA1_ENC, "EAP-AES128" => AUTH_EAP_AES128, "EAP-AES128-PLUS" => AUTH_EAP_AES128_PLUS, "ECDH-X25519-CHALLENGE" => AUTH_ECDH_X25519_CHALLENGE, "ECDSA-NIST256P-CHALLENGE" => AUTH_ECDSA_NIST256P_CHALLENGE, "EXTERNAL" => AUTH_EXTERNAL, "GS2-KRB5" => AUTH_GS2_KRB5, "GS2-KRB5-PLUS" => AUTH_GS2_KRB5_PLUS, "GSS-SPNEGO" => AUTH_GSS_SPNEGO, "GSSAPI" => AUTH_GSSAPI, "KERBEROS_V4" => AUTH_KERBEROS_V4, "KERBEROS_V5" => AUTH_KERBEROS_V5, "NMAS-SAMBA-AUTH" => AUTH_NMAS_SAMBA_AUTH, "NMAS_AUTHEN" => AUTH_NMAS_AUTHEN, "NMAS_LOGIN" => AUTH_NMAS_LOGIN, "NTLM" => AUTH_NTLM, "OAUTH10A" => AUTH_OAUTH10A, "OPENID20" => AUTH_OPENID20, "OTP" => AUTH_OTP, "SAML20" => AUTH_SAML20, "SECURID" => AUTH_SECURID, "SKEY" => AUTH_SKEY, "SPNEGO" => AUTH_SPNEGO, "SPNEGO-PLUS" => AUTH_SPNEGO_PLUS, "SXOVER-PLUS" => AUTH_SXOVER_PLUS, "CRAM-MD5" => AUTH_CRAM_MD5, "DIGEST-MD5" => AUTH_DIGEST_MD5, "ANONYMOUS" => AUTH_ANONYMOUS,*/ _ => return Err(format!("Unsupported mechanism {:?}.", value)), })) } } impl<'x> TryFrom> for Mechanism { type Error = (); fn try_from(value: Variable<'x>) -> Result { match value { Variable::Constant(value) => Mechanism::try_from(value), Variable::Array(items) => { let mut mechanism = 0; for item in items { match item { Variable::Constant(value) => mechanism |= Mechanism::try_from(value)?.0, _ => return Err(()), } } Ok(Mechanism(mechanism)) } _ => Err(()), } } } impl TryFrom for Mechanism { type Error = (); fn try_from(value: ExpressionConstant) -> Result { match value { ExpressionConstant::Login => Ok(Mechanism(AUTH_LOGIN)), ExpressionConstant::Plain => Ok(Mechanism(AUTH_PLAIN)), ExpressionConstant::Xoauth2 => Ok(Mechanism(AUTH_XOAUTH2)), ExpressionConstant::Oauthbearer => Ok(Mechanism(AUTH_OAUTHBEARER)), _ => Err(()), } } } impl From for u64 { fn from(value: Mechanism) -> Self { value.0 } } impl From for Mechanism { fn from(value: u64) -> Self { Mechanism(value) } } impl<'x> TryFrom> for MtPriority { type Error = (); fn try_from(value: Variable<'x>) -> Result { match value { Variable::Constant(value) => match value { ExpressionConstant::Mixer => Ok(MtPriority::Mixer), ExpressionConstant::Stanag4406 => Ok(MtPriority::Stanag4406), ExpressionConstant::Nsep => Ok(MtPriority::Nsep), _ => Err(()), }, Variable::String(value) => { let value = value.as_str(); if value.eq_ignore_ascii_case("MIXER") { Ok(MtPriority::Mixer) } else if value.eq_ignore_ascii_case("STANAG4406") { Ok(MtPriority::Stanag4406) } else if value.eq_ignore_ascii_case("NSEP") { Ok(MtPriority::Nsep) } else { Err(()) } } _ => Err(()), } } } impl From for Stage { fn from(value: MtaStage) -> Self { match value { MtaStage::Connect => Stage::Connect, MtaStage::Ehlo => Stage::Ehlo, MtaStage::Auth => Stage::Auth, MtaStage::Mail => Stage::Mail, MtaStage::Rcpt => Stage::Rcpt, MtaStage::Data => Stage::Data, } } }