Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
568 lines
20 KiB
Rust
568 lines
20 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* 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<Policy>,
|
|
|
|
pub milters: Vec<Milter>,
|
|
pub hooks: Vec<MTAHook>,
|
|
}
|
|
|
|
#[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<SocketAddr>,
|
|
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<u32>,
|
|
pub flags_protocol: Option<u32>,
|
|
pub run_on_stage: AHashSet<Stage>,
|
|
}
|
|
|
|
#[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<Stage>,
|
|
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::<MtaInboundSession>().await;
|
|
let connect = bp.setting_infallible::<MtaStageConnect>().await;
|
|
let auth = bp.setting_infallible::<MtaStageAuth>().await;
|
|
let ehlo = bp.setting_infallible::<MtaStageEhlo>().await;
|
|
let mail = bp.setting_infallible::<MtaStageMail>().await;
|
|
let rcpt = bp.setting_infallible::<MtaStageRcpt>().await;
|
|
let data = bp.setting_infallible::<MtaStageData>().await;
|
|
let ext = bp.setting_infallible::<MtaExtensions>().await;
|
|
|
|
let mut hooks = Vec::new();
|
|
|
|
for hook in bp.list_infallible::<MtaHook>().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::<MtaMilter>()
|
|
.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<Self, Self::Err> {
|
|
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<Variable<'x>> for Mechanism {
|
|
type Error = ();
|
|
|
|
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
|
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<ExpressionConstant> for Mechanism {
|
|
type Error = ();
|
|
|
|
fn try_from(value: ExpressionConstant) -> Result<Self, Self::Error> {
|
|
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<Mechanism> for u64 {
|
|
fn from(value: Mechanism) -> Self {
|
|
value.0
|
|
}
|
|
}
|
|
|
|
impl From<u64> for Mechanism {
|
|
fn from(value: u64) -> Self {
|
|
Mechanism(value)
|
|
}
|
|
}
|
|
|
|
impl<'x> TryFrom<Variable<'x>> for MtPriority {
|
|
type Error = ();
|
|
|
|
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
|
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<MtaStage> 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,
|
|
}
|
|
}
|
|
}
|