use std::borrow::Cow;
use std::io;
use std::pin::{pin, Pin};
use std::time::Duration;
use log::{info, warn};
use tokio::io::AsyncWriteExt;
use super::{
command_processor::CommandProcessor,
request_reader::{
AppendContinuation, CommandStart, CompressionStatus, RequestReader,
},
response_writer::{self, OutputControl, OutputDisconnect, OutputEvent},
};
use crate::{
imap::syntax as s,
support::{append_limit::APPEND_SIZE_LIMIT, async_io::ServerIo},
};
pub async fn run(mut io: ServerIo, mut processor: CommandProcessor) {
let (input_result, output_result) =
run_impl(io.clone(), &mut processor).await;
let _ = tokio::time::timeout(Duration::from_secs(5), io.shutdown()).await;
match (input_result, output_result) {
(Some(Ok(())), _) => {
info!("{} Normal client disconnect", processor.log_prefix());
},
(Some(Err(ProcessError::Protocol)), _) => {
warn!("{} Unrecoverable protocol error", processor.log_prefix());
},
(Some(Err(ProcessError::Loitering)), _) => {
warn!("{} Disconnected due to loitering", processor.log_prefix());
},
(Some(Err(ProcessError::InputIo(e))), _) | (_, Some(Err(e)))
if io::ErrorKind::UnexpectedEof == e.kind() =>
{
info!("{} Client closed connection", processor.log_prefix());
}
(Some(Err(ProcessError::InputIo(e))), _) | (_, Some(Err(e)))
if io::ErrorKind::TimedOut == e.kind() =>
{
info!("{} Network timed out", processor.log_prefix());
}
(Some(Err(ProcessError::InputIo(e))), _) => {
warn!(
"{} Disconnected due to input I/O error: {e}",
processor.log_prefix(),
);
},
(_, Some(Err(e))) => {
warn!(
"{} Disconnected due to output I/O error: {e}",
processor.log_prefix(),
);
},
(_, Some(Ok(OutputDisconnect::ByControl))) => {
warn!(
"{} Connection terminated by server logic",
processor.log_prefix(),
);
},
(Some(Err(ProcessError::OutputClosed)), _) |
(_, Some(Ok(OutputDisconnect::InputClosed))) |
(None, None) => {
warn!(
"{} Disconnected for unknown reason",
processor.log_prefix(),
);
}
}
}
async fn run_impl(
io: ServerIo,
processor: &mut CommandProcessor,
) -> (
Option<Result<(), ProcessError>>,
Option<io::Result<OutputDisconnect>>,
) {
let (output_tx, output_rx) = tokio::sync::mpsc::channel(16);
let (ping_tx, ping_rx) = tokio::sync::mpsc::channel(1);
let inactivity_monitor = inactivity_monitor(ping_rx, output_tx.clone());
let mut input_processor =
pin!(process_input(io.clone(), processor, ping_tx, output_tx));
let mut response_writer =
pin!(response_writer::write_responses(io, output_rx));
tokio::select! {
_ = inactivity_monitor => {
let output_result = tokio::time::timeout(
Duration::from_secs(5),
response_writer).await.ok();
(Some(Err(ProcessError::Loitering)), output_result)
},
input_result = &mut input_processor => {
let output_result = tokio::time::timeout(
Duration::from_secs(5),
response_writer).await.ok();
(Some(input_result), output_result)
},
output_result = &mut response_writer => {
let input_result = tokio::time::timeout(
Duration::from_secs(5),
input_processor).await.ok();
(input_result, Some(output_result))
},
}
}
macro_rules! bye {
($output_tx:expr, $err:expr, $code:expr, $quip:expr $(,)*) => {
let _ = $output_tx
.send(OutputEvent::ResponseLine {
line: s::ResponseLine {
tag: None,
response: s::Response::Cond(s::CondResponse {
cond: s::RespCondType::Bye,
code: $code,
quip: Some(Cow::Borrowed($quip)),
}),
},
ctl: OutputControl::Disconnect,
})
.await;
return Err($err);
};
}
macro_rules! send_cond {
($output_tx:expr, $tag:expr, $cond:ident, $code:expr, $quip:expr $(,)*) => {
$output_tx
.send(OutputEvent::ResponseLine {
line: s::ResponseLine {
tag: Some($tag),
response: s::Response::Cond(s::CondResponse {
cond: s::RespCondType::$cond,
code: $code,
quip: Some(Cow::Borrowed($quip)),
}),
},
ctl: OutputControl::Flush,
})
.await
.map_err(|_| ProcessError::OutputClosed)
};
}
async fn process_input(
io: ServerIo,
processor: &mut CommandProcessor,
ping_tx: tokio::sync::mpsc::Sender<bool>,
mut output_tx: tokio::sync::mpsc::Sender<OutputEvent>,
) -> Result<(), ProcessError> {
let mut request_reader = RequestReader::new(io);
let mut unauthenticated_commands = 0;
output_tx
.send(OutputEvent::ResponseLine {
line: processor.greet(),
ctl: OutputControl::Flush,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
while !processor.logged_out() {
let authenticated = processor.is_authenticated();
let _ = ping_tx.send(authenticated).await;
if !authenticated {
unauthenticated_commands += 1;
if unauthenticated_commands > 30 {
bye!(
output_tx,
ProcessError::Loitering,
None,
"LOGIN or GET OUT",
);
}
}
let start = request_reader
.read_command_start(&mut output_tx, authenticated)
.await
.map_err(ProcessError::InputIo)?;
match start {
CommandStart::Incomprehensible => {
bye!(
output_tx,
ProcessError::Protocol,
Some(s::RespTextCode::Parse(())),
"That doesn't look anything like an IMAP command!",
);
},
CommandStart::Bad(tag) => {
send_cond!(
output_tx,
Cow::Owned(tag),
Bad,
Some(s::RespTextCode::Parse(())),
"Unrecognised command syntax",
)?;
},
CommandStart::TooLongRecovered(tag) => {
send_cond!(
output_tx,
Cow::Owned(tag),
No,
None,
"Command line too long",
)?;
},
CommandStart::TooLongFatal(_tag) => {
bye!(
output_tx,
ProcessError::Protocol,
None,
"Command line too long",
);
},
CommandStart::OutputDisconnected => {
return Err(ProcessError::OutputClosed);
},
CommandStart::AppendStart {
append,
size,
literal_plus,
} => {
let append = s::AppendCommandStart::<'static> {
tag: Cow::Owned(append.tag.into_owned()),
mailbox: append.mailbox.into_static(),
first_fragment: append.first_fragment,
};
handle_append(
&mut request_reader,
&mut output_tx,
processor,
append,
size,
literal_plus,
)
.await?;
},
CommandStart::AuthenticateStart(auth) => {
if let Some(line) = processor.authenticate_start(&auth) {
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&line.response),
line,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
continue;
}
let auth = s::AuthenticateCommandStart::<'static> {
tag: Cow::Owned(auth.tag.into_owned()),
auth_type: Cow::Owned(auth.auth_type.into_owned()),
initial_response: auth
.initial_response
.map(|ir| Cow::Owned(ir.into_owned())),
};
output_tx
.send(OutputEvent::ContinuationLine {
prompt: "",
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
let Some(auth_data) = request_reader
.read_raw_line()
.await
.map_err(ProcessError::InputIo)?
else {
bye!(
output_tx,
ProcessError::Protocol,
None,
"Auth data too long",
);
};
let line = processor.authenticate_finish(auth, auth_data);
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&line.response),
line,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
},
CommandStart::StandAlone(cmd) => match cmd {
s::CommandLine {
tag,
cmd: s::Command::Simple(s::SimpleCommand::Compress),
} => {
let tag = tag.into_owned();
handle_compress(&mut request_reader, &mut output_tx, tag)
.await?;
},
s::CommandLine {
tag,
cmd: s::Command::Simple(s::SimpleCommand::Idle),
} => {
let tag = tag.into_owned();
handle_idle(
&mut request_reader,
&mut output_tx,
processor,
tag,
)
.await?;
},
cmd => {
let line =
processor.handle_command(cmd, output_tx.clone()).await;
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&line.response),
line,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
},
},
}
}
Ok(())
}
enum ProcessError {
InputIo(io::Error),
Protocol,
Loitering,
OutputClosed,
}
async fn handle_append(
request_reader: &mut RequestReader<ServerIo>,
output_tx: &mut tokio::sync::mpsc::Sender<OutputEvent>,
processor: &mut CommandProcessor,
append: s::AppendCommandStart<'_>,
mut size: u32,
mut literal_plus: bool,
) -> Result<(), ProcessError> {
let tag = append.tag.clone().into_owned();
if let Err(e) = processor.cmd_append_start(&append) {
request_reader
.abort_append(size, literal_plus)
.await
.map_err(ProcessError::InputIo)?;
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&e),
line: s::ResponseLine {
tag: Some(Cow::Owned(tag)),
response: e,
},
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
return Ok(());
}
let mut fragment = append.first_fragment;
loop {
if 0 == size {
send_cond!(
output_tx,
Cow::Owned(tag),
No,
None,
"Zero-size APPEND",
)?;
request_reader
.abort_append(size, literal_plus)
.await
.map_err(ProcessError::InputIo)?;
return Ok(());
}
if size > APPEND_SIZE_LIMIT {
send_cond!(
output_tx,
Cow::Owned(tag),
No,
Some(s::RespTextCode::TooBig(())),
"APPEND message too big",
)?;
request_reader
.abort_append(size, literal_plus)
.await
.map_err(ProcessError::InputIo)?;
return Ok(());
}
if !literal_plus {
output_tx
.send(OutputEvent::ContinuationLine { prompt: "go" })
.await
.map_err(|_| ProcessError::OutputClosed)?;
}
let result = {
let mut reader = request_reader.read_append_literal(size);
let result = processor
.cmd_append_item(&fragment, size, Pin::new(&mut reader))
.await;
tokio::io::copy(&mut reader, &mut tokio::io::sink())
.await
.map_err(ProcessError::InputIo)?;
result
};
if let Err(response) = result {
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&response),
line: s::ResponseLine {
tag: Some(Cow::Owned(tag)),
response,
},
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
processor.cmd_append_abort();
request_reader
.abort_append_after_literal()
.await
.map_err(ProcessError::InputIo)?;
return Ok(());
}
let next = request_reader
.continue_append(fragment.utf8)
.await
.map_err(ProcessError::InputIo)?;
match next {
AppendContinuation::NextPart {
fragment: f,
size: s,
literal_plus: l,
} => {
fragment = f;
size = s;
literal_plus = l;
},
AppendContinuation::Done => break,
AppendContinuation::SyntaxError => {
processor.cmd_append_abort();
send_cond!(
output_tx,
Cow::Owned(tag),
Bad,
Some(s::RespTextCode::Parse(())),
"Bad APPEND continuation",
)?;
return Ok(());
},
AppendContinuation::TooLong => {
processor.cmd_append_abort();
send_cond!(
output_tx,
Cow::Owned(tag),
Bad,
None,
"APPEND continuation line too long",
)?;
return Ok(());
},
}
}
let line = processor
.cmd_append_commit(Cow::Owned(tag), output_tx.clone())
.await;
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&line.response),
line,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
Ok(())
}
async fn handle_compress(
request_reader: &mut RequestReader<ServerIo>,
output_tx: &mut tokio::sync::mpsc::Sender<OutputEvent>,
tag: String,
) -> Result<(), ProcessError> {
let response = match request_reader.start_compression() {
CompressionStatus::Started => s::CondResponse {
cond: s::RespCondType::Ok,
code: None,
quip: Some(Cow::Borrowed("Oo.")),
},
CompressionStatus::AlreadyActive => s::CondResponse {
cond: s::RespCondType::No,
code: Some(s::RespTextCode::CompressionActive(())),
quip: Some(Cow::Borrowed("Already compressing")),
},
CompressionStatus::InvalidPipelinedData => s::CondResponse {
cond: s::RespCondType::Bad,
code: Some(s::RespTextCode::ClientBug(())),
quip: Some(Cow::Borrowed(
"There is pipelined data behind the \
COMPRESS command",
)),
},
};
output_tx
.send(OutputEvent::ResponseLine {
ctl: if s::RespCondType::Ok == response.cond {
OutputControl::EnableCompression
} else {
OutputControl::Flush
},
line: s::ResponseLine {
tag: Some(Cow::Owned(tag)),
response: s::Response::Cond(response),
},
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
Ok(())
}
async fn handle_idle(
request_reader: &mut RequestReader<ServerIo>,
output_tx: &mut tokio::sync::mpsc::Sender<OutputEvent>,
processor: &mut CommandProcessor,
tag: String,
) -> Result<(), ProcessError> {
if let Some(line) = processor.cmd_idle_preflight(&tag) {
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&line.response),
line,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
return Ok(());
}
output_tx
.send(OutputEvent::ContinuationLine { prompt: "idling" })
.await
.map_err(|_| ProcessError::OutputClosed)?;
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
let mut idle = pin!(processor.cmd_idle(&tag, output_tx.clone(), cancel_rx));
let read_line = async move {
let line = request_reader.read_raw_line().await;
let _ = cancel_tx.send(());
line
};
let fatal_error = tokio::select! {
idle_error = &mut idle => {
Some(idle_error)
},
line = read_line => {
let line = line.map_err(ProcessError::InputIo)?;
if line.is_some() {
None
} else {
Some(s::ResponseLine {
tag: None,
response: s::Response::Cond(s::CondResponse {
cond: s::RespCondType::Bye,
code: None,
quip: Some(Cow::Borrowed(
"Expecting 'DONE', got far more than that",
)),
}),
})
}
},
};
if let Some(fatal_error) = fatal_error {
output_tx
.send(OutputEvent::ResponseLine {
line: fatal_error,
ctl: OutputControl::Disconnect,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
return Err(ProcessError::Protocol);
}
let line = idle.await;
output_tx
.send(OutputEvent::ResponseLine {
ctl: command_end_ctl(&line.response),
line,
})
.await
.map_err(|_| ProcessError::OutputClosed)?;
Ok(())
}
async fn inactivity_monitor(
mut ping: tokio::sync::mpsc::Receiver<bool>,
output_tx: tokio::sync::mpsc::Sender<OutputEvent>,
) {
let mut authenticated = false;
loop {
let timeout = if authenticated {
Duration::from_secs(31 * 60)
} else {
Duration::from_secs(300)
};
tokio::select! {
_ = tokio::time::sleep(timeout) => break,
auth = ping.recv() => authenticated = auth.unwrap_or(false),
}
}
let _ = output_tx
.send(OutputEvent::ResponseLine {
ctl: OutputControl::Disconnect,
line: s::ResponseLine {
tag: None,
response: s::Response::Cond(s::CondResponse {
cond: s::RespCondType::Bye,
code: None,
quip: Some(Cow::Borrowed(if authenticated {
"Inactivity timer elapsed"
} else {
"Authentication timed out"
})),
}),
},
})
.await;
}
fn command_end_ctl(response: &s::Response<'_>) -> OutputControl {
if matches!(
*response,
s::Response::Cond(s::CondResponse {
cond: s::RespCondType::Bye,
..
}),
) {
OutputControl::Disconnect
} else {
OutputControl::Flush
}
}