/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::{inbound::auth::SaslToken, queue::QueueId}; use common::{ Inner, Server, auth::AccountInfo, config::smtp::auth::VerifyStrategy, network::{ServerInstance, asn::AsnGeoLookupResult}, }; use email::message::delivery::ORCPT_ADDR_TYPE; use mail_auth::{IprevOutput, SpfOutput}; use smtp_proto::request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver, }; use std::{ hash::Hash, net::IpAddr, sync::Arc, time::{Duration, Instant}, }; use tokio::io::{AsyncRead, AsyncWrite}; use utils::DomainPart; pub mod params; pub mod throttle; #[derive(Clone)] pub struct SmtpSessionManager { pub inner: Arc, } impl SmtpSessionManager { pub fn new(inner: Arc) -> Self { Self { inner } } } pub enum State { Request(RequestReceiver), Bdat(BdatReceiver), Data(DataReceiver), Sasl(LineReceiver), SkipData(DummyDataReceiver, &'static [u8]), RequestTooLarge(DummyLineReceiver), Accepted(QueueId), None, } pub struct Session { pub hostname: String, pub state: State, pub instance: Arc, pub server: Server, pub stream: T, pub data: SessionData, pub params: SessionParameters, } pub struct SessionData { pub session_id: u64, pub local_ip: IpAddr, pub local_ip_str: String, pub local_port: u16, pub remote_ip: IpAddr, pub remote_ip_str: String, pub remote_port: u16, pub asn_geo_data: AsnGeoLookupResult, pub helo_domain: String, pub mail_from: Option, pub rcpt_to: Vec, pub rcpt_errors: usize, pub rcpt_oks: usize, pub message: Vec, pub authenticated_as: Option, pub auth_errors: usize, pub priority: i16, pub delivery_by: i64, pub future_release: u64, pub valid_until: Instant, pub bytes_left: usize, pub messages_sent: usize, pub iprev: Option, pub spf_ehlo: Option, pub spf_mail_from: Option, pub dnsbl_error: Option>, } #[derive(Clone, Debug)] pub struct SessionAddress { pub address: String, pub address_lcase: String, pub domain: String, pub flags: u64, pub dsn_info: Option, } #[derive(Debug, Default)] pub struct SessionParameters { // Global parameters pub timeout: Duration, // Ehlo parameters pub ehlo_require: bool, pub ehlo_reject_non_fqdn: bool, // Auth parameters pub auth_require: bool, pub auth_errors_max: usize, pub auth_errors_wait: Duration, // Rcpt parameters pub rcpt_errors_max: usize, pub rcpt_errors_wait: Duration, pub rcpt_max: usize, pub rcpt_dsn: bool, pub can_expn: bool, pub can_vrfy: bool, pub max_message_size: usize, // Mail authentication parameters pub iprev: VerifyStrategy, pub spf_ehlo: VerifyStrategy, pub spf_mail_from: VerifyStrategy, } impl SessionData { pub fn new( local_ip: IpAddr, local_port: u16, remote_ip: IpAddr, remote_port: u16, asn_geo_data: AsnGeoLookupResult, session_id: u64, ) -> Self { SessionData { session_id, local_ip, local_port, remote_ip, local_ip_str: local_ip.to_string(), remote_ip_str: remote_ip.to_string(), remote_port, asn_geo_data, helo_domain: String::new(), mail_from: None, rcpt_to: Vec::new(), authenticated_as: None, priority: 0, valid_until: Instant::now(), rcpt_errors: 0, rcpt_oks: 0, message: Vec::with_capacity(0), auth_errors: 0, messages_sent: 0, bytes_left: 0, delivery_by: 0, future_release: 0, iprev: None, spf_ehlo: None, spf_mail_from: None, dnsbl_error: None, } } } impl Default for State { fn default() -> Self { State::Request(RequestReceiver::default()) } } impl PartialEq for SessionAddress { fn eq(&self, other: &Self) -> bool { self.address_lcase == other.address_lcase } } impl Eq for SessionAddress {} impl Hash for SessionAddress { fn hash(&self, state: &mut H) { self.address_lcase.hash(state); } } impl Ord for SessionAddress { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match self.domain.cmp(&other.domain) { std::cmp::Ordering::Equal => self.address_lcase.cmp(&other.address_lcase), order => order, } } } impl PartialOrd for SessionAddress { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Session { pub fn local( server: Server, instance: std::sync::Arc, data: SessionData, ) -> Self { Session { hostname: "localhost".into(), state: State::None, instance, server, stream: common::network::stream::NullIo::default(), data, params: SessionParameters { timeout: Default::default(), ehlo_require: Default::default(), ehlo_reject_non_fqdn: Default::default(), auth_require: Default::default(), auth_errors_max: Default::default(), auth_errors_wait: Default::default(), rcpt_errors_max: Default::default(), rcpt_errors_wait: Default::default(), rcpt_max: Default::default(), rcpt_dsn: Default::default(), max_message_size: Default::default(), iprev: VerifyStrategy::Disable, spf_ehlo: VerifyStrategy::Disable, spf_mail_from: VerifyStrategy::Disable, can_expn: false, can_vrfy: false, }, } } pub fn has_failed(&mut self) -> Option { if self.stream.tx_buf.first().is_none_or(|&c| c == b'2') { self.stream.tx_buf.clear(); None } else { let response = std::str::from_utf8(&self.stream.tx_buf) .unwrap() .trim() .into(); self.stream.tx_buf.clear(); Some(response) } } } impl SessionData { pub fn local( authenticated_as: AccountInfo, mail_from: Option, rcpt_to: Vec, message: Vec, session_id: u64, ) -> Self { SessionData { local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), local_ip_str: "127.0.0.1".into(), remote_ip_str: "127.0.0.1".into(), remote_port: 0, local_port: 0, session_id, asn_geo_data: AsnGeoLookupResult::default(), helo_domain: "localhost".into(), mail_from, rcpt_to, rcpt_errors: 0, rcpt_oks: 0, message, authenticated_as: Some(authenticated_as), auth_errors: 0, priority: 0, delivery_by: 0, future_release: 0, valid_until: Instant::now(), bytes_left: 0, messages_sent: 0, iprev: None, spf_ehlo: None, spf_mail_from: None, dnsbl_error: None, } } } impl SessionAddress { pub fn new(address: String) -> Self { let address_lcase = address.to_lowercase(); SessionAddress { domain: address_lcase.domain_part().into(), address_lcase, address, flags: 0, dsn_info: None, } } pub fn orig_address(&self) -> &str { self.dsn_info.as_deref().unwrap_or(&self.address_lcase) } pub fn orcpt_parameter(&self) -> Option { self.dsn_info .as_deref() .map(|orcpt| format!("{ORCPT_ADDR_TYPE}{}", orcpt.to_lowercase())) } }