use std::borrow::Cow;
use std::io;
use std::pin::Pin;
use std::str;
use std::task;
use std::time::{Duration, Instant};
use log::{error, info, warn};
use openssl::ssl::SslAcceptor;
use tokio::io::{
AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufStream, DuplexStream,
};
use tokio::sync::{mpsc, oneshot};
use super::super::{codes::*, syntax::*};
use super::bridge::*;
use crate::support::{
append_limit::APPEND_SIZE_LIMIT, async_io::ServerIo, error::Error,
log_prefix::LogPrefix,
};
pub(super) struct Service {
pub(super) lmtp: bool,
pub(super) offer_binarymime: bool,
pub(super) auth: bool,
pub(super) send_request: mpsc::Sender<Request>,
}
struct Server {
io: BufStream<ServerIo>,
log_prefix: LogPrefix,
ssl_acceptor: Option<SslAcceptor>,
service: Service,
local_host_name: String,
ineffective_commands: u32,
deadline_tx: mpsc::Sender<Instant>,
quit: bool,
has_helo: bool,
has_mail_from: bool,
has_auth: bool,
recipients: u32,
sending_data: Option<SendData>,
unix_newlines: bool,
}
pub(super) async fn run(
io: ServerIo,
log_prefix: LogPrefix,
ssl_acceptor: Option<SslAcceptor>,
service: Service,
local_host_name: String,
) -> Result<(), Error> {
let (deadline_tx, deadline_rx) = mpsc::channel(1);
let mut server = Server {
io: BufStream::new(io),
log_prefix,
ssl_acceptor,
service,
local_host_name,
ineffective_commands: 0,
deadline_tx,
quit: false,
has_helo: false,
has_mail_from: false,
has_auth: false,
recipients: 0,
sending_data: None,
unix_newlines: false,
};
tokio::select! {
r = server.run() => r,
_ = idle_timer(deadline_rx) => {
Err(Error::Io(io::Error::new(
io::ErrorKind::TimedOut,
"Connection idle timer expired",
)))
},
}
}
struct SendData {
stream: DuplexStream,
recipient_responses:
oneshot::Sender<mpsc::Sender<Result<(), SmtpResponse<'static>>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ResponseKind {
Final,
Urgent,
Delayable,
}
impl ResponseKind {
fn or_final(self, phinal: bool) -> Self {
if phinal {
ResponseKind::Final
} else {
self
}
}
fn indicator(self) -> char {
match self {
Final => ' ',
Urgent | Delayable => '-',
}
}
}
use self::ResponseKind::*;
macro_rules! require {
($this:expr, $($fns:ident = $arg:expr,)* @else $el:block) => {
$(if let Some(r) = $this.$fns($arg).await { $el; return r; })*
};
($this:expr, $($fns:ident = $arg:expr),*) => {
require!($this, $($fns = $arg,)* @else {})
};
}
const MAX_LINE: usize = 1024;
static EXTENSIONS: &[&str] = &[
"8BITMIME", "AUTH PLAIN",
"BINARYMIME", "CHUNKING", "ENHANCEDSTATUSCODES", "PIPELINING",
concat_appendlimit!("SIZE "),
"SMTPUTF8", "STARTTLS",
"HELP", ];
impl Server {
pub(super) async fn run(&mut self) -> Result<(), Error> {
self.send_greeting().await?;
let mut buffer = Vec::new();
while !self.quit {
self.run_command(&mut buffer).await?;
}
Ok(())
}
async fn run_command(&mut self, buffer: &mut Vec<u8>) -> Result<(), Error> {
let _ = self
.deadline_tx
.send(Instant::now() + Duration::from_secs(60))
.await;
buffer.clear();
(&mut self.io)
.take(MAX_LINE as u64)
.read_until(b'\n', buffer)
.await?;
if buffer.is_empty() {
return Err(Error::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"EOF reached at start of command",
)));
}
if !buffer.ends_with(b"\n") {
if buffer.len() >= MAX_LINE {
self.send_response(
Final,
pc::CommandSyntaxError,
Some((cc::PermFail, sc::OtherProtocolStatus)),
Cow::Borrowed("Command line too long"),
)
.await?;
while !buffer.is_empty() && !buffer.ends_with(b"\n") {
buffer.clear();
(&mut self.io)
.take(MAX_LINE as u64)
.read_until(b'\n', buffer)
.await?;
}
return Ok(());
} else {
return Err(Error::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"EOF reached within command",
)));
}
}
self.ineffective_commands += 1;
if self.ineffective_commands > 30 {
warn!(
"{} Terminating connection after too many non-mail commands",
self.log_prefix,
);
self.send_response(
Final,
pc::ServiceClosing,
None,
Cow::Borrowed("Too many commands issued without sending mail"),
)
.await?;
self.quit = true;
return Ok(());
}
let line_ending_len = if buffer.ends_with(b"\r\n") {
2
} else {
self.unix_newlines = true;
1
};
let command_line = &buffer[..buffer.len() - line_ending_len];
if command_line.contains(&0) {
warn!(
"{} Remote is speaking binary, closing connection",
self.log_prefix,
);
self.quit = true;
return Ok(());
}
let command_line = match str::from_utf8(command_line) {
Ok(s) => s,
Err(_) => {
warn!("{} Non-UTF-8 command received", self.log_prefix);
self.send_response(
Final,
pc::CommandSyntaxError,
Some((cc::PermFail, sc::OtherProtocolStatus)),
Cow::Borrowed("Malformed UTF-8"),
)
.await?;
return Ok(());
},
};
let command = match command_line.parse::<Command>() {
Ok(c) => c,
Err(_) => {
let mut debug_line = command_line;
if let Some((truncate_len, _)) =
debug_line.char_indices().nth(64)
{
debug_line = &debug_line[..truncate_len];
}
warn!(
"{} Received bad command {debug_line:?}",
self.log_prefix
);
if looks_like_known_command(command_line) {
self.send_response(
Final,
pc::ParameterSyntaxError,
Some((cc::PermFail, sc::InvalidCommandArguments)),
Cow::Borrowed("Unknown command syntax"),
)
.await?;
} else {
self.send_response(
Final,
pc::CommandSyntaxError,
Some((cc::PermFail, sc::InvalidCommand)),
Cow::Borrowed("Unrecognised command"),
)
.await?;
}
return Ok(());
},
};
match command {
Command::Helo(command, origin) => {
self.cmd_helo(command, origin).await
},
Command::Auth(mechanism, data) => {
self.cmd_auth(mechanism, data).await
},
Command::MailFrom(email, size, warnings) => {
for warning in warnings {
warn!("{} {}", self.log_prefix, warning);
}
self.cmd_mail_from(email, size).await
},
Command::Recipient(email, warnings) => {
for warning in warnings {
warn!("{} {}", self.log_prefix, warning);
}
self.cmd_recipient(email).await
},
Command::Data => self.cmd_data().await,
Command::BinaryData(len, last) => {
self.cmd_binary_data(len, last).await
},
Command::Reset => self.cmd_reset().await,
Command::Verify => self.cmd_verify().await,
Command::Expand => self.cmd_expand().await,
Command::Help => self.cmd_help().await,
Command::Noop => self.cmd_noop().await,
Command::Quit => self.cmd_quit().await,
Command::StartTls => self.cmd_start_tls().await,
Command::Http => self.cmd_http(),
}
}
async fn cmd_helo(
&mut self,
command: String,
origin: String,
) -> Result<(), Error> {
require!(self, need_helo = false);
let extended = !"HELO".eq_ignore_ascii_case(&command);
self.log_prefix.set_helo(origin.clone());
info!("{} SMTP {command}", self.log_prefix);
if !self
.service_request(RequestPayload::Helo(HeloRequest {
command,
host: origin.clone(),
tls: self.io.get_ref().ssl_string(),
}))
.await?
{
return Ok(());
}
self.send_response(
Delayable.or_final(!extended),
pc::Ok,
None,
Cow::Owned(format!(
"{} salutations, {}",
self.local_host_name, origin
)),
)
.await?;
self.has_helo = true;
if extended {
for (ix, &ext) in EXTENSIONS.iter().enumerate() {
if "STARTTLS" == ext
&& (self.io.get_ref().is_ssl()
|| self.ssl_acceptor.is_none())
{
continue;
}
if "BINARYMIME" == ext && !self.service.offer_binarymime {
continue;
}
if ext.starts_with("AUTH ")
&& (!self.service.auth || !self.io.get_ref().is_ssl())
{
continue;
}
self.send_response(
Delayable.or_final(ix + 1 == EXTENSIONS.len()),
pc::Ok,
None,
Cow::Borrowed(ext),
)
.await?;
}
}
Ok(())
}
async fn cmd_auth(
&mut self,
mechanism: String,
data: Option<String>,
) -> Result<(), Error> {
require!(self, need_helo = true, need_mail_from = false);
if !self.io.get_ref().is_ssl() {
warn!("{} Rejected attempt to AUTH without TLS", self.log_prefix);
return self.send_response(
Final,
pc::EncryptionRequiredForRequestedAuthenticationMechanism,
Some((cc::PermFail, sc::EncryptionRequiredForRequestedAuthenticationMechanism)),
Cow::Borrowed("Have you no shame?"),
).await;
}
if !self.service.auth {
warn!(
"{} Rejected attempt to AUTH on an unauthenticated service",
self.log_prefix,
);
return self
.send_response(
Final,
pc::CommandNotImplemented,
Some((cc::PermFail, sc::SecurityFeaturesNotSupported)),
Cow::Borrowed("Authentication is not supported here"),
)
.await;
}
if self.has_auth {
return self
.send_response(
Final,
pc::BadSequenceOfCommands,
None,
Cow::Borrowed("Already authenticated"),
)
.await;
}
if !mechanism.eq_ignore_ascii_case("PLAIN") {
warn!(
"{} Rejected attempt to auth with method {mechanism:?}",
self.log_prefix,
);
return self
.send_response(
Final,
pc::CommandParameterNotImplemented,
Some((cc::PermFail, sc::InvalidCommandArguments)),
Cow::Borrowed("Unsupported AUTH mechanism"),
)
.await;
}
let data = match data {
Some(data) if data != "=" => data,
_ => {
self.send_response(
Final,
pc::ServerChallenge,
None,
Cow::Borrowed(""),
)
.await?;
let mut buffer = Vec::new();
(&mut self.io)
.take(MAX_LINE as u64)
.read_until(b'\n', &mut buffer)
.await?;
if !buffer.ends_with(b"\n") {
self.send_response(
Final,
pc::CommandSyntaxError,
Some((
cc::PermFail,
sc::AuthenticationExchangeLineTooLong,
)),
Cow::Borrowed("Line too long"),
)
.await?;
return Err(Error::Io(io::Error::other(
"Authentication line too long",
)));
}
let _ = buffer.pop();
if Some(&b'\r') == buffer.last() {
let _ = buffer.pop();
}
String::from_utf8_lossy(&buffer).into_owned()
},
};
if data.is_empty() || data == "=" {
return self
.send_response(
Final,
pc::ParameterSyntaxError,
Some((cc::PermFail, sc::SyntaxError)),
Cow::Borrowed("The empty string is not valid for PLAIN"),
)
.await;
}
if data == "*" {
return self
.send_response(
Final,
pc::ParameterSyntaxError,
None,
Cow::Borrowed("SASL aborted"),
)
.await;
}
let Some(data) = base64::decode(&data)
.ok()
.and_then(|d| String::from_utf8(d).ok())
else {
return self
.send_response(
Final,
pc::CommandSyntaxError,
Some((cc::PermFail, sc::SyntaxError)),
Cow::Borrowed("Invalid base64"),
)
.await;
};
let mut parts = data.split('\x00');
let (Some(authorise), Some(authenticate), Some(password), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return self
.send_response(
Final,
pc::CommandSyntaxError,
Some((cc::PermFail, sc::SyntaxError)),
Cow::Borrowed("Invalid auth syntax"),
)
.await;
};
if !authorise.is_empty() && authorise != authenticate {
return self
.send_response(
Final,
pc::AuthenticationCredentialsInvalid,
Some((cc::PermFail, sc::AuthenticationCredentialsInvalid)),
Cow::Borrowed("authorise-id must match authenticate-id"),
)
.await;
}
if self
.service_request(RequestPayload::Auth(AuthRequest {
userid: authenticate.to_owned(),
password: password.to_owned(),
}))
.await?
{
self.has_auth = true;
self.send_response(
Final,
pc::AuthenticationSucceeded,
Some((cc::Success, sc::OtherSecurity)),
Cow::Borrowed("OK"),
)
.await?;
}
Ok(())
}
async fn cmd_mail_from(
&mut self,
return_path: String,
approx_size: Option<u64>,
) -> Result<(), Error> {
require!(self, need_helo = true, need_mail_from = false);
if self.service.auth && !self.has_auth {
return self
.send_response(
Final,
pc::AuthenticationRequired,
Some((cc::PermFail, sc::DeliveryNotAuthorised)),
Cow::Borrowed("Authentication required"),
)
.await;
}
if approx_size.unwrap_or(0) > APPEND_SIZE_LIMIT as u64 {
return self
.send_response(
Final,
pc::ExceededStorageAllocation,
Some((cc::PermFail, sc::MessageLengthExceedsLimit)),
Cow::Owned(format!(
"Maximum message size is {} bytes",
APPEND_SIZE_LIMIT
)),
)
.await;
}
if !self
.service_request(RequestPayload::Mail(MailRequest {
from: return_path,
}))
.await?
{
return Ok(());
}
info!("{} Start mail transaction", self.log_prefix);
self.ineffective_commands = 0;
self.has_mail_from = true;
self.send_response(
Final,
pc::Ok,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("OK"),
)
.await
}
async fn cmd_recipient(
&mut self,
forward_path: String,
) -> Result<(), Error> {
require!(
self,
need_helo = true,
need_mail_from = true,
need_data = false
);
if !self
.service_request(RequestPayload::Recipient(RecipientRequest {
to: forward_path,
}))
.await?
{
return Ok(());
}
self.ineffective_commands = 0;
self.recipients += 1;
self.send_response(
Final,
pc::Ok,
Some((cc::Success, sc::DestinationAddressValid)),
Cow::Borrowed("OK"),
)
.await
}
async fn start_data_transfer(&mut self) -> Result<bool, Error> {
let (data_in, data_out) = tokio::io::duplex(4096);
let (recipients_tx, recipients_rx) = oneshot::channel();
if !self
.service_request(RequestPayload::Data(DataRequest {
data: data_in,
recipient_responses: recipients_rx,
}))
.await?
{
return Ok(false);
}
self.sending_data = Some(SendData {
stream: data_out,
recipient_responses: recipients_tx,
});
Ok(true)
}
async fn complete_data_transfer(&mut self) -> Result<(), Error> {
let sending_data = self.sending_data.take().unwrap();
drop(sending_data.stream);
let (recipients_tx, mut recipients_rx) = mpsc::channel(1);
let _ = sending_data.recipient_responses.send(recipients_tx);
let need_responses = if self.service.lmtp {
self.recipients
} else {
1
};
let mut success = false;
for i in 0..need_responses {
let response = recipients_rx
.recv()
.await
.unwrap_or_else(|| {
error!(
"{} [BUG] Service worker disappeared during data transfer",
self.log_prefix,
);
Err(SmtpResponse(
pc::TransactionFailed,
Some((cc::TempFail, sc::OtherMailSystem)),
Cow::Borrowed("Internal server error"),
))
})
.err()
.unwrap_or(SmtpResponse(
pc::Ok,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("OK"),
));
success |= (200..=299).contains(&(response.0 as i32));
self.send_response(
Urgent.or_final(i + 1 == need_responses),
response.0,
response.1,
response.2,
)
.await?;
}
info!(
"{} Completed data transfer {}",
self.log_prefix,
if success {
"successfully"
} else {
"unsuccessfully"
},
);
self.recipients = 0;
self.has_mail_from = false;
Ok(())
}
async fn cmd_data(&mut self) -> Result<(), Error> {
require!(
self,
need_helo = true,
need_mail_from = true,
need_recipients = true,
need_data = false
);
if !self.start_data_transfer().await? {
return Ok(());
}
self.ineffective_commands = 0;
self.send_response(
Final,
pc::StartMailInput,
None,
Cow::Borrowed("Go ahead"),
)
.await?;
info!("{} Begin legacy-format data transfer", self.log_prefix);
let _ = self
.deadline_tx
.send(Instant::now() + Duration::from_secs(1800))
.await;
{
let sending_data = self.sending_data.as_mut().unwrap();
copy_with_dot_stuffing(
Pin::new(&mut DiscardOnError(&mut sending_data.stream)),
Pin::new(&mut self.io),
self.unix_newlines,
true,
)
.await?;
}
self.complete_data_transfer().await
}
async fn cmd_binary_data(
&mut self,
len: u64,
last: bool,
) -> Result<(), Error> {
let _ = self
.deadline_tx
.send(Instant::now() + Duration::from_secs(30 + len / 4000))
.await;
let mut consumed = false;
let result = self.cmd_binary_data_impl(&mut consumed, len, last).await;
if !consumed {
tokio::io::copy(
&mut (&mut self.io).take(len),
&mut tokio::io::sink(),
)
.await?;
}
result
}
async fn cmd_binary_data_impl(
&mut self,
consumed: &mut bool,
len: u64,
last: bool,
) -> Result<(), Error> {
require!(
self,
need_helo = true,
need_mail_from = true,
need_recipients = true
);
self.ineffective_commands = 0;
if self.sending_data.is_none() {
if !self.start_data_transfer().await? {
return Ok(());
}
info!("{} Begin binary data transfer", self.log_prefix);
}
let abort = {
let mut src = (&mut self.io).take(len);
let sending_data = self.sending_data.as_mut().unwrap();
let result =
tokio::io::copy(&mut src, &mut sending_data.stream).await;
let _ = tokio::io::copy(&mut src, &mut tokio::io::sink()).await;
*consumed = true;
match result {
Ok(_) => false,
Err(e) if io::ErrorKind::BrokenPipe == e.kind() => true,
Err(e) => return Err(Error::Io(e)),
}
};
if last || abort {
self.complete_data_transfer().await
} else {
self.send_response(
Final,
pc::Ok,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("OK"),
)
.await
}
}
async fn cmd_reset(&mut self) -> Result<(), Error> {
self.has_mail_from = false;
self.recipients = 0;
self.sending_data = None;
if self.service_request(RequestPayload::Reset).await? {
self.send_response(
Final,
pc::Ok,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("OK"),
)
.await?;
}
Ok(())
}
async fn cmd_verify(&mut self) -> Result<(), Error> {
info!("{} Rejected attempt to use VRFY", self.log_prefix);
self.send_response(
Final,
pc::CannotVerify,
Some((cc::Success, sc::OtherSecurity)),
Cow::Borrowed("VRFY not supported"),
)
.await
}
async fn cmd_expand(&mut self) -> Result<(), Error> {
self.send_response(
Final,
pc::ActionNotTakenPermanent,
Some((cc::PermFail, sc::SystemNotCapableOfSelectedFeatures)),
Cow::Borrowed("There are no mailing lists here"),
)
.await
}
async fn cmd_help(&mut self) -> Result<(), Error> {
self.send_response(
Delayable,
pc::HelpMessage,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("You asked me for help"),
)
.await?;
self.send_response(
Delayable,
pc::HelpMessage,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("An SMTP server!"),
)
.await?;
self.send_response(
Delayable,
pc::HelpMessage,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("What a strange life choice"),
)
.await?;
self.send_response(
Delayable,
pc::HelpMessage,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("This is the Crymap SMTP server."),
)
.await?;
self.send_response(
Final,
pc::HelpMessage,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("End of HELP"),
)
.await
}
async fn cmd_noop(&mut self) -> Result<(), Error> {
self.send_response(
Final,
pc::Ok,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("OK"),
)
.await
}
async fn cmd_quit(&mut self) -> Result<(), Error> {
self.quit = true;
let _ = self
.send_response(
Final,
pc::ServiceClosing,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("Bye"),
)
.await;
Ok(())
}
async fn cmd_start_tls(&mut self) -> Result<(), Error> {
require!(
self,
need_helo = true,
need_tls = false,
need_mail_from = false,
need_recipients = false,
need_data = false
);
if self.ssl_acceptor.is_none() {
self.send_response(
Final,
pc::ActionNotTakenPermanent,
None,
Cow::Borrowed("TLS not configured"),
)
.await?;
return Ok(());
}
self.send_response(
Final,
pc::ServiceReady,
Some((cc::Success, sc::Undefined)),
Cow::Borrowed("Switching to TLS"),
)
.await?;
info!("{} Start TLS handshake", self.log_prefix);
self.has_helo = false;
self.io
.get_mut()
.ssl_accept(&self.ssl_acceptor.take().unwrap())
.await?;
info!("{} TLS handshake completed", self.log_prefix);
Ok(())
}
fn cmd_http(&mut self) -> Result<(), Error> {
warn!(
"{} Client made an HTTP request, aborting the connection",
self.log_prefix,
);
self.quit = true;
Ok(())
}
async fn need_helo(&mut self, present: bool) -> Option<Result<(), Error>> {
self.check_need(
self.has_helo,
present,
"Already got HELO",
"Still waiting for HELO",
)
.await
}
async fn need_mail_from(
&mut self,
present: bool,
) -> Option<Result<(), Error>> {
self.check_need(
self.has_mail_from,
present,
"Already got MAIL FROM",
"Still waiting for MAIL FROM",
)
.await
}
async fn need_recipients(
&mut self,
present: bool,
) -> Option<Result<(), Error>> {
self.check_need(
self.recipients > 0,
present,
"Already have recipients",
"No recipients",
)
.await
}
async fn need_data(&mut self, present: bool) -> Option<Result<(), Error>> {
self.check_need(
self.sending_data.is_some(),
present,
"Already transferring data",
"Not currently transferring data",
)
.await
}
async fn need_tls(&mut self, present: bool) -> Option<Result<(), Error>> {
self.check_need(
self.sending_data.is_some(),
present,
"Already using TLS",
"Not using TLS",
)
.await
}
async fn check_need(
&mut self,
current_status: bool,
desired_status: bool,
message_if_already_present: &str,
message_if_missing: &str,
) -> Option<Result<(), Error>> {
if current_status != desired_status {
Some(
self.send_response(
Final,
pc::BadSequenceOfCommands,
Some((cc::PermFail, sc::InvalidCommand)),
Cow::Borrowed(if current_status {
message_if_already_present
} else {
message_if_missing
}),
)
.await,
)
} else {
None
}
}
async fn send_greeting(&mut self) -> Result<(), Error> {
self.send_response(
Final,
pc::ServiceReady,
None,
Cow::Owned(format!(
"{} {} {} {}.{}.{} ready",
self.local_host_name,
match (self.service.lmtp, self.io.get_ref().is_ssl()) {
(false, false) => "ESMTP",
(false, true) => "ESMTPS",
(true, false) => "LMTP",
(true, true) => "LMTPS",
},
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR"),
env!("CARGO_PKG_VERSION_PATCH"),
)),
)
.await
}
async fn send_response(
&mut self,
kind: ResponseKind,
primary_code: PrimaryCode,
secondary_code: Option<(ClassCode, SubjectCode)>,
quip: Cow<'_, str>,
) -> Result<(), Error> {
use std::fmt::Write as _;
if primary_code == pc::ServiceClosing
|| primary_code == pc::ServiceNotAvailableClosing
{
self.quit = true;
}
let mut s = String::new();
let _ = write!(s, "{}{}", primary_code as u16, kind.indicator());
if let Some((class, subject)) = secondary_code {
let subject = subject as u16;
let split = if subject >= 100 { 100 } else { 10 };
let _ = write!(
s,
"{}.{}.{} ",
class as u8,
subject / split,
subject % split
);
}
let _ = write!(s, "{}\r\n", quip);
self.io.write_all(s.as_bytes()).await?;
match kind {
Final | Urgent => self.io.flush().await?,
Delayable => (),
}
Ok(())
}
async fn service_request(
&mut self,
payload: RequestPayload,
) -> Result<bool, Error> {
let (response_tx, response_rx) = oneshot::channel();
if self
.service
.send_request
.send(Request {
payload,
respond: response_tx,
})
.await
.is_err()
{
error!("{} [BUG] Service worker disappeared", self.log_prefix);
self.send_response(
Final,
pc::ServiceNotAvailableClosing,
Some((cc::TempFail, sc::OtherMailSystem)),
Cow::Borrowed("Internal server error"),
)
.await?;
return Ok(false);
}
let Ok(result) = response_rx.await else {
error!("{} [BUG] Service worker disappeared", self.log_prefix);
self.send_response(
Final,
pc::ServiceNotAvailableClosing,
Some((cc::TempFail, sc::OtherMailSystem)),
Cow::Borrowed("Internal server error"),
)
.await?;
return Ok(false);
};
if let Err(e) = result {
self.send_response(Final, e.0, e.1, e.2).await?;
return Ok(false);
}
Ok(true)
}
}
struct DiscardOnError<'a>(&'a mut DuplexStream);
impl tokio::io::AsyncWrite for DiscardOnError<'_> {
fn poll_write(
self: Pin<&mut Self>,
ctx: &mut task::Context<'_>,
buf: &[u8],
) -> task::Poll<io::Result<usize>> {
match Pin::new(&mut *self.get_mut().0).poll_write(ctx, buf) {
task::Poll::Ready(Err(_)) => task::Poll::Ready(Ok(buf.len())),
poll => poll,
}
}
fn poll_flush(
self: Pin<&mut Self>,
ctx: &mut task::Context<'_>,
) -> task::Poll<io::Result<()>> {
Pin::new(&mut *self.get_mut().0).poll_flush(ctx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
ctx: &mut task::Context<'_>,
) -> task::Poll<io::Result<()>> {
Pin::new(&mut *self.get_mut().0).poll_shutdown(ctx)
}
}
async fn copy_with_dot_stuffing(
mut dst: Pin<&mut impl AsyncWriteExt>,
mut src: Pin<&mut impl AsyncBufReadExt>,
mut unix_lines: bool,
mut detect_line_endings: bool,
) -> io::Result<()> {
async fn write_with_line_conversion(
mut dst: Pin<&mut impl AsyncWriteExt>,
data: &[u8],
has_trailing_cr: bool,
unix_lines: bool,
) -> io::Result<()> {
if unix_lines
&& data.ends_with(b"\n")
&& !data.ends_with(b"\r\n")
&& (!has_trailing_cr || b"\n" != data)
{
dst.as_mut().write_all(&data[..data.len() - 1]).await?;
dst.as_mut().write_all(b"\r\n").await?;
} else {
dst.write_all(data).await?;
}
Ok(())
}
let mut start_of_line = true;
let mut has_trailing_cr = false;
loop {
let mut src_buffer = src.as_mut();
let mut buffer = src_buffer.fill_buf().await?;
if buffer.is_empty() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"EOF encountered in DATA payload",
));
}
if let Some(eol) = memchr::memchr(b'\n', buffer) {
buffer = &buffer[..=eol];
if detect_line_endings {
if !buffer.ends_with(b"\r\n") && !has_trailing_cr {
unix_lines = true;
}
detect_line_endings = false;
}
}
let buffer_len = buffer.len();
if start_of_line {
if b".\r\n" == buffer || b".\n" == buffer {
src.as_mut().consume(buffer_len);
break;
}
if b".\r" == buffer {
src.as_mut().consume(buffer_len);
let mut extra = [0u8; 1];
src.as_mut().read_exact(&mut extra).await?;
if b'\n' == extra[0] {
break;
}
dst.write_all(b"\r").await?;
dst.write_all(&extra).await?;
has_trailing_cr = b'\r' == extra[0];
start_of_line = false;
continue;
}
if b"." == buffer {
src.as_mut().consume(buffer_len);
let mut extra = [0u8; 2];
src.as_mut().read_exact(&mut extra[..1]).await?;
if b'\n' == extra[0] {
break;
}
src.as_mut().read_exact(&mut extra[1..]).await?;
if b"\r\n" == &extra {
break;
}
write_with_line_conversion(
dst.as_mut(),
&extra,
false, unix_lines,
)
.await?;
has_trailing_cr = extra.ends_with(b"\r");
start_of_line = unix_lines && extra.ends_with(b"\n");
continue;
}
}
let line_contents = if b'.' == buffer[0] && start_of_line {
&buffer[1..]
} else {
buffer
};
write_with_line_conversion(
dst.as_mut(),
line_contents,
has_trailing_cr,
unix_lines,
)
.await?;
start_of_line = buffer.ends_with(b"\r\n")
|| (b"\n" == buffer && has_trailing_cr)
|| (unix_lines && buffer.ends_with(b"\n"));
has_trailing_cr = buffer.ends_with(b"\r");
src.as_mut().consume(buffer_len);
}
Ok(())
}
async fn idle_timer(mut deadline_rx: mpsc::Receiver<Instant>) {
let mut deadline = Instant::now() + Duration::from_secs(30);
loop {
match tokio::time::timeout_at(deadline.into(), deadline_rx.recv()).await
{
Err(_) => return, Ok(None) => return, Ok(Some(d)) => deadline = d,
}
}
}
#[cfg(test)]
mod test {
use proptest::prelude::*;
use super::*;
fn copy_with_dot_stuffing_sync(
stuffed: &[u8],
buffer_size: usize,
unix_lines: bool,
detect_line_endings: bool,
) -> Vec<u8> {
let mut decoded_bytes = Vec::<u8>::new();
let mut reader =
tokio::io::BufReader::with_capacity(buffer_size, stuffed);
futures::executor::block_on(copy_with_dot_stuffing(
Pin::new(&mut decoded_bytes),
Pin::new(&mut reader),
unix_lines,
detect_line_endings,
))
.unwrap();
decoded_bytes
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 4096,
..ProptestConfig::default()
})]
#[test]
fn binary_dot_stuffing_decodes_properly(
content in "[x.\r\n]{0,100}\r\n",
buffer_size in 1usize..=32,
) {
let mut stuffed = content.replace("\r\n.", "\r\n..");
if stuffed.starts_with(".") {
stuffed = format!(".{}", stuffed);
}
stuffed.push_str(".\r\n");
let decoded_bytes = copy_with_dot_stuffing_sync(
stuffed.as_bytes(),
buffer_size,
false,
false,
);
assert_eq!(content, str::from_utf8(&decoded_bytes).unwrap());
}
#[test]
fn text_dot_stuffing_decodes_properly(
content in "[x.\r\n]{0,100}\r\n",
buffer_size in 1usize..=32,
) {
let mut stuffed = content.replace("\n.", "\n..");
if stuffed.starts_with(".") {
stuffed = format!(".{}", stuffed);
}
stuffed.push_str(".\n");
let decoded_bytes = copy_with_dot_stuffing_sync(
stuffed.as_bytes(),
buffer_size,
true,
false,
);
let converted_content = content.replace("\r\n", "\n")
.replace("\n", "\r\n");
assert_eq!(
converted_content,
str::from_utf8(&decoded_bytes).unwrap(),
);
}
}
#[test]
fn dot_stuffing_line_ending_detection() {
assert_eq!(
b"foo\r\nbar\n.\r\n".to_vec(),
copy_with_dot_stuffing_sync(
b"foo\r\nbar\n.\r\n.\r\n",
64,
false,
true,
),
);
assert_eq!(
b"foo\r\nbar\r\nbaz\r\n".to_vec(),
copy_with_dot_stuffing_sync(
b"foo\nbar\r\nbaz\n.\n",
64,
false,
true,
),
);
}
}