Import upstream v0.16.22, stripped

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.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "pop3"
version = "0.16.22"
edition = "2024"
[dependencies]
store = { path = "../store" }
common = { path = "../common" }
directory = { path = "../directory" }
imap = { path = "../imap" }
utils = { path = "../utils" }
trc = { path = "../trc" }
types = { path = "../types" }
email = { path = "../email" }
registry = { path = "../registry" }
mail-parser = { version = "0.11", features = ["full_encoding"] }
tokio = { version = "1.53", features = ["full"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] }
[features]
test_mode = []
[lints]
workspace = true
+269
View File
@@ -0,0 +1,269 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Session, State,
protocol::{Command, Mechanism, request::Error},
};
use common::{
KV_RATE_LIMIT_IMAP,
network::{SessionResult, SessionStream},
};
use directory::Credentials;
use trc::{AddContext, SecurityEvent};
impl<T: SessionStream> Session<T> {
pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult {
trc::event!(
Pop3(trc::Pop3Event::RawInput),
SpanId = self.session_id,
Size = bytes.len(),
Contents = trc::Value::from_maybe_string(bytes),
);
let mut bytes = bytes.iter();
let mut requests = Vec::with_capacity(2);
loop {
match self.receiver.parse(&mut bytes) {
Ok(request) => {
// Group delete requests when possible
match (request, requests.last_mut()) {
(Command::Dele { msg }, Some(Ok(Command::DeleMany { msgs }))) => {
msgs.push(msg);
}
(Command::Dele { msg }, Some(Ok(Command::Dele { msg: other_msg }))) => {
let request = Ok(Command::DeleMany {
msgs: vec![*other_msg, msg],
});
requests.pop();
requests.push(request);
}
(request, _) => {
requests.push(Ok(request));
}
}
}
Err(Error::NeedsMoreData) => {
break;
}
Err(Error::Parse(err)) => {
// Check for port scanners
if matches!(&self.state, State::NotAuthenticated { .. },) {
match self.server.is_scanner_fail2banned(self.remote_addr).await {
Ok(true) => {
trc::event!(
Security(SecurityEvent::ScanBan),
SpanId = self.session_id,
RemoteIp = self.remote_addr,
Reason = "Invalid POP3 command",
);
return SessionResult::Close;
}
Ok(false) => {}
Err(err) => {
trc::error!(
err.span_id(self.session_id)
.details("Failed to check for fail2ban")
);
}
}
}
requests.push(Err(trc::Pop3Event::Error.into_err().details(err)));
}
}
}
for request in requests {
let result = match request {
Ok(command) => match self.validate_request(command).await {
Ok(command) => match command {
Command::User { name } => {
if let State::NotAuthenticated { username, .. } = &mut self.state {
let response = format!("{name} is a valid mailbox");
*username = Some(name);
self.write_ok(response)
.await
.map(|_| SessionResult::Continue)
} else {
unreachable!();
}
}
Command::Pass { string } => {
let username =
if let State::NotAuthenticated { username, .. } = &mut self.state {
username.take().unwrap()
} else {
unreachable!()
};
Box::pin(self.handle_auth(Credentials::Basic {
username,
secret: string,
mfa_token: None,
}))
.await
.map(|_| SessionResult::Continue)
}
Command::Quit => self.handle_quit().await.map(|_| SessionResult::Close),
Command::Stat => self.handle_stat().await.map(|_| SessionResult::Continue),
Command::List { msg } => {
self.handle_list(msg).await.map(|_| SessionResult::Continue)
}
Command::Retr { msg } => self
.handle_fetch(msg, None)
.await
.map(|_| SessionResult::Continue),
Command::Dele { msg } => self
.handle_dele(vec![msg])
.await
.map(|_| SessionResult::Continue),
Command::DeleMany { msgs } => self
.handle_dele(msgs)
.await
.map(|_| SessionResult::Continue),
Command::Top { msg, n } => self
.handle_fetch(msg, n.into())
.await
.map(|_| SessionResult::Continue),
Command::Uidl { msg } => {
self.handle_uidl(msg).await.map(|_| SessionResult::Continue)
}
Command::Noop => {
trc::event!(
Pop3(trc::Pop3Event::Noop),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("NOOP").await.map(|_| SessionResult::Continue)
}
Command::Rset => self.handle_rset().await.map(|_| SessionResult::Continue),
Command::Capa => self.handle_capa().await.map(|_| SessionResult::Continue),
Command::Stls => {
self.handle_stls().await.map(|_| SessionResult::UpgradeTls)
}
Command::Utf8 => self.handle_utf8().await.map(|_| SessionResult::Continue),
Command::Auth { mechanism, params } => {
Box::pin(self.handle_sasl(mechanism, params))
.await
.map(|_| SessionResult::Continue)
}
Command::Apop { .. } => Err(trc::Pop3Event::Error
.into_err()
.details("APOP not supported.")),
},
Err(err) => Err(err),
},
Err(err) => Err(err),
};
match result {
Ok(SessionResult::Continue) => (),
Ok(result) => return result,
Err(err) => {
if !self.write_err(err).await {
return SessionResult::Close;
}
}
}
}
SessionResult::Continue
}
async fn validate_request(
&self,
command: Command<String, Mechanism>,
) -> trc::Result<Command<String, Mechanism>> {
match &command {
Command::Capa | Command::Quit | Command::Noop => Ok(command),
Command::Auth {
mechanism: Mechanism::Plain,
..
}
| Command::User { .. }
| Command::Pass { .. }
| Command::Apop { .. } => {
if let State::NotAuthenticated { username, .. } = &self.state {
if self.stream.is_tls() || self.server.core.imap.allow_plain_auth {
if !matches!(command, Command::Pass { .. }) || username.is_some() {
Ok(command)
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Username was not provided."))
}
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Cannot authenticate over plain-text."))
}
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Already authenticated."))
}
}
Command::Auth { .. } => {
if let State::NotAuthenticated { .. } = &self.state {
Ok(command)
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Already authenticated."))
}
}
Command::Stls => {
if !self.stream.is_tls() {
Ok(command)
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Already in TLS mode."))
}
}
Command::List { .. }
| Command::Retr { .. }
| Command::Dele { .. }
| Command::DeleMany { .. }
| Command::Top { .. }
| Command::Uidl { .. }
| Command::Utf8
| Command::Stat
| Command::Rset => {
if let State::Authenticated { mailbox, .. } = &self.state {
if let Some(rate) = &self.server.core.imap.rate_requests {
if self
.server
.in_memory_store()
.is_rate_allowed(
KV_RATE_LIMIT_IMAP,
&mailbox.account_id.to_be_bytes(),
rate,
true,
)
.await
.caused_by(trc::location!())?
.is_none()
{
Ok(command)
} else {
Err(trc::LimitEvent::TooManyRequests.into_err())
}
} else {
Ok(command)
}
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Not authenticated."))
}
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#![warn(clippy::large_futures)]
use std::{net::IpAddr, sync::Arc};
use common::{
Inner, Server,
auth::AccessToken,
network::{ServerInstance, SessionStream, limiter::InFlight},
};
use mailbox::Mailbox;
use protocol::request::Parser;
pub mod client;
pub mod mailbox;
pub mod op;
pub mod protocol;
pub mod session;
static SERVER_GREETING: &str = "+OK Stalwart POP3 at your service.\r\n";
#[derive(Clone)]
pub struct Pop3SessionManager {
pub inner: Arc<Inner>,
}
impl Pop3SessionManager {
pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}
pub struct Session<T: SessionStream> {
pub server: Server,
pub instance: Arc<ServerInstance>,
pub receiver: Parser,
pub state: State,
pub stream: T,
pub in_flight: InFlight,
pub remote_addr: IpAddr,
pub session_id: u64,
}
pub enum State {
NotAuthenticated {
auth_failures: u32,
username: Option<String>,
},
Authenticated {
mailbox: Mailbox,
in_flight: Option<InFlight>,
access_token: AccessToken,
},
}
impl State {
pub fn mailbox(&self) -> &Mailbox {
match self {
State::Authenticated { mailbox, .. } => mailbox,
_ => unreachable!(),
}
}
pub fn mailbox_mut(&mut self) -> &mut Mailbox {
match self {
State::Authenticated { mailbox, .. } => mailbox,
_ => unreachable!(),
}
}
pub fn access_token(&self) -> &AccessToken {
match self {
State::Authenticated { access_token, .. } => access_token,
_ => unreachable!(),
}
}
}
+85
View File
@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::Session;
use common::network::SessionStream;
use email::{
cache::{MessageCacheFetch, mailbox::MailboxCacheAccess},
mailbox::INBOX_ID,
};
use std::collections::BTreeMap;
use trc::AddContext;
use types::special_use::SpecialUse;
#[derive(Default)]
pub struct Mailbox {
pub messages: Vec<Message>,
pub account_id: u32,
pub uid_validity: u32,
pub total: u32,
pub size: u32,
}
pub struct Message {
pub id: u32,
pub uid: u32,
pub size: u32,
pub deleted: bool,
}
impl<T: SessionStream> Session<T> {
pub async fn fetch_mailbox(&self, account_id: u32) -> trc::Result<Mailbox> {
// Obtain UID validity
let cache = self
.server
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
if cache.emails.items.is_empty() {
return Ok(Mailbox::default());
}
let uid_validity = cache
.mailbox_by_role(&SpecialUse::Inbox)
.map(|x| x.uid_validity)
.unwrap_or_default();
// Sort by UID
let message_map = cache
.emails
.items
.iter()
.filter_map(|message| {
message
.mailboxes
.iter()
.find(|m| m.mailbox_id == INBOX_ID)
.map(|m| (m.uid, (message.document_id, message.size)))
})
.collect::<BTreeMap<u32, (u32, u32)>>();
// Create mailbox
let mut mailbox = Mailbox {
messages: Vec::with_capacity(message_map.len()),
uid_validity,
account_id,
..Default::default()
};
for (uid, (id, size)) in message_map {
mailbox.messages.push(Message {
id,
uid,
size,
deleted: false,
});
mailbox.total += 1;
mailbox.size += size;
}
Ok(mailbox)
}
}
+115
View File
@@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Session, State,
protocol::{Command, Mechanism, request},
};
use common::{
auth::AuthRequest,
network::{SessionStream, limiter::LimiterResult},
};
use directory::Credentials;
use mail_parser::decoders::base64::base64_decode;
use registry::schema::enums::Permission;
impl<T: SessionStream> Session<T> {
pub async fn handle_sasl(
&mut self,
mechanism: Mechanism,
mut params: Vec<String>,
) -> trc::Result<()> {
match mechanism {
Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => {
if !params.is_empty() {
let credentials = base64_decode(params.pop().unwrap().as_bytes())
.and_then(|challenge| {
if mechanism == Mechanism::Plain {
Credentials::decode_sasl_challenge_plain(&challenge)
} else {
Credentials::decode_sasl_challenge_oauth(&challenge)
}
})
.ok_or_else(|| {
trc::AuthEvent::Error
.into_err()
.details("Invalid SASL challenge")
})?;
Box::pin(self.handle_auth(credentials)).await
} else {
// TODO: This hack is temporary until the SASL library is developed
self.receiver.state = request::State::Argument {
request: Command::Auth {
mechanism: mechanism.as_str().as_bytes().to_vec(),
params: vec![],
},
num: 1,
last_is_space: true,
};
self.write_bytes("+\r\n").await
}
}
_ => Err(trc::AuthEvent::Error
.into_err()
.details("Authentication mechanism not supported.")),
}
}
pub async fn handle_auth(&mut self, credentials: Credentials) -> trc::Result<()> {
// Authenticate
let access_token = self
.server
.authenticate(&AuthRequest::from_credentials(
credentials,
self.session_id,
self.remote_addr,
))
.await
.map_err(|err| {
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) {
match &self.state {
State::NotAuthenticated {
auth_failures,
username,
} if *auth_failures < self.server.core.imap.max_auth_failures => {
self.state = State::NotAuthenticated {
auth_failures: auth_failures + 1,
username: username.clone(),
};
}
_ => {
return trc::AuthEvent::TooManyAttempts.into_err().caused_by(err);
}
}
}
err
})
.and_then(|token| token.assert_has_permission(Permission::Pop3Authenticate))?;
// Enforce concurrency limits
let in_flight = match access_token.is_imap_request_allowed() {
LimiterResult::Allowed(in_flight) => Some(in_flight),
LimiterResult::Forbidden => {
return Err(trc::LimitEvent::ConcurrentRequest.into_err());
}
LimiterResult::Disabled => None,
};
// Fetch mailbox
let mailbox = self.fetch_mailbox(access_token.account_id()).await?;
// Create session
self.state = State::Authenticated {
in_flight,
mailbox,
access_token,
};
self.write_ok("Authentication successful").await
}
}
+137
View File
@@ -0,0 +1,137 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Instant;
use common::network::SessionStream;
use email::message::delete::EmailDeletion;
use registry::schema::enums::Permission;
use store::{roaring::RoaringBitmap, write::BatchBuilder};
use trc::AddContext;
use crate::{Session, State, protocol::response::Response};
impl<T: SessionStream> Session<T> {
pub async fn handle_dele(&mut self, msgs: Vec<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.enforce_permission(Permission::Pop3Dele)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox_mut();
let mut response = Vec::new();
for msg in &msgs {
if let Some(message) = mailbox.messages.get_mut(msg.saturating_sub(1) as usize) {
if !message.deleted {
response.extend_from_slice(format!("+OK message {msg} deleted\r\n").as_bytes());
message.deleted = true;
} else {
response.extend_from_slice(
format!("-ERR message {msg} already deleted\r\n").as_bytes(),
);
}
} else {
response.extend_from_slice("-ERR no such message\r\n".as_bytes());
}
}
trc::event!(
Pop3(trc::Pop3Event::Delete),
SpanId = self.session_id,
Total = msgs.len(),
Elapsed = op_start.elapsed()
);
self.write_bytes(response).await
}
pub async fn handle_rset(&mut self) -> trc::Result<()> {
let op_start = Instant::now();
let mut count = 0;
let mailbox = self.state.mailbox_mut();
for message in &mut mailbox.messages {
if message.deleted {
count += 1;
message.deleted = false;
}
}
trc::event!(
Pop3(trc::Pop3Event::Reset),
SpanId = self.session_id,
Total = count as u64,
Elapsed = op_start.elapsed()
);
self.write_ok(format!("{count} messages undeleted")).await
}
pub async fn handle_quit(&mut self) -> trc::Result<()> {
let op_start = Instant::now();
let mut deleted_docs = Vec::new();
if let State::Authenticated { mailbox, .. } = &self.state {
let mut deleted = RoaringBitmap::new();
for message in &mailbox.messages {
if message.deleted {
deleted.insert(message.id);
deleted_docs.push(trc::Value::from(message.id));
}
}
if !deleted.is_empty() {
let num_deleted = deleted.len();
let mut batch = BatchBuilder::new();
let not_deleted = self
.server
.emails_delete(
mailbox.account_id,
self.state.access_token().tenant_id(),
&mut batch,
deleted,
)
.await
.caused_by(trc::location!())?;
if !batch.is_empty() {
self.server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
self.server.notify_task_queue();
}
if not_deleted.is_empty() {
self.write_ok(format!(
"Stalwart POP3 bids you farewell ({num_deleted} messages deleted)."
))
.await?;
} else {
self.write_bytes(
Response::Err::<u32>("Some messages could not be deleted".into())
.serialize(),
)
.await?;
}
} else {
self.write_ok("Stalwart POP3 bids you farewell (no messages deleted).")
.await?;
}
} else {
self.write_ok("Stalwart POP3 bids you farewell.").await?;
}
trc::event!(
Pop3(trc::Pop3Event::Quit),
SpanId = self.session_id,
DocumentId = deleted_docs,
Elapsed = op_start.elapsed()
);
Ok(())
}
}
+91
View File
@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{Session, protocol::response::Response};
use common::network::SessionStream;
use email::message::metadata::MessageMetadata;
use registry::schema::enums::Permission;
use std::time::Instant;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{collection::Collection, field::EmailField};
use utils::chained_bytes::ChainedBytes;
impl<T: SessionStream> Session<T> {
pub async fn handle_fetch(&mut self, msg: u32, lines: Option<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.enforce_permission(Permission::Pop3Retr)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) {
if let Some(metadata_) = self
.server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
mailbox.account_id,
Collection::Email,
message.id,
EmailField::Metadata,
))
.await
.caused_by(trc::location!())?
{
let metadata = metadata_
.unarchive::<MessageMetadata>()
.caused_by(trc::location!())?;
if let Some(bytes) = self
.server
.blob_store()
.get_blob(metadata.blob_hash.0.as_slice(), 0..usize::MAX)
.await
.caused_by(trc::location!())?
{
trc::event!(
Pop3(trc::Pop3Event::Fetch),
SpanId = self.session_id,
DocumentId = message.id,
Elapsed = op_start.elapsed()
);
let bytes = ChainedBytes::new(metadata.raw_headers.as_ref())
.with_last(
bytes
.get(metadata.blob_body_offset.to_native() as usize..)
.unwrap_or_default(),
)
.get_full_range();
self.write_bytes(
Response::Message::<u32> {
bytes,
lines: lines.unwrap_or(0),
}
.serialize(),
)
.await
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Failed to fetch message. Perhaps another session deleted it?")
.caused_by(trc::location!()))
}
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("Failed to fetch message. Perhaps another session deleted it?")
.caused_by(trc::location!()))
}
} else {
Err(trc::Pop3Event::Error.into_err().details("No such message."))
}
}
}
+125
View File
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Instant;
use common::network::SessionStream;
use registry::schema::enums::Permission;
use crate::{Session, protocol::response::Response};
impl<T: SessionStream> Session<T> {
pub async fn handle_list(&mut self, msg: Option<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.enforce_permission(Permission::Pop3List)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(msg) = msg {
if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) {
trc::event!(
Pop3(trc::Pop3Event::ListMessage),
SpanId = self.session_id,
DocumentId = message.id,
Size = message.size,
Elapsed = op_start.elapsed()
);
self.write_ok(format!("{} {}", msg, message.size)).await
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("No such message.")
.caused_by(trc::location!()))
}
} else {
trc::event!(
Pop3(trc::Pop3Event::List),
SpanId = self.session_id,
Total = mailbox.messages.len(),
Elapsed = op_start.elapsed()
);
self.write_bytes(
Response::List(mailbox.messages.iter().map(|m| m.size).collect::<Vec<_>>())
.serialize(),
)
.await
}
}
pub async fn handle_uidl(&mut self, msg: Option<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.enforce_permission(Permission::Pop3Uidl)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(msg) = msg {
if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) {
trc::event!(
Pop3(trc::Pop3Event::UidlMessage),
SpanId = self.session_id,
DocumentId = message.id,
Uid = message.uid,
UidValidity = mailbox.uid_validity,
Elapsed = op_start.elapsed()
);
self.write_ok(format!("{} {}{}", msg, mailbox.uid_validity, message.uid))
.await
} else {
Err(trc::Pop3Event::Error
.into_err()
.details("No such message.")
.caused_by(trc::location!()))
}
} else {
trc::event!(
Pop3(trc::Pop3Event::Uidl),
SpanId = self.session_id,
Total = mailbox.messages.len(),
Elapsed = op_start.elapsed()
);
self.write_bytes(
Response::List(
mailbox
.messages
.iter()
.map(|m| format!("{}{}", mailbox.uid_validity, m.uid))
.collect::<Vec<_>>(),
)
.serialize(),
)
.await
}
}
pub async fn handle_stat(&mut self) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.enforce_permission(Permission::Pop3Stat)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
trc::event!(
Pop3(trc::Pop3Event::Stat),
SpanId = self.session_id,
Total = mailbox.total,
Size = mailbox.size,
Elapsed = op_start.elapsed()
);
self.write_ok(format!("{} {}", mailbox.total, mailbox.size))
.await
}
}
+64
View File
@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::network::SessionStream;
use crate::{
Session,
protocol::{Mechanism, response::Response},
};
pub mod authenticate;
pub mod delete;
pub mod fetch;
pub mod list;
impl<T: SessionStream> Session<T> {
pub async fn handle_capa(&mut self) -> trc::Result<()> {
let mechanisms = if self.stream.is_tls() || self.server.core.imap.allow_plain_auth {
vec![Mechanism::Plain, Mechanism::OAuthBearer, Mechanism::XOauth2]
} else {
vec![Mechanism::OAuthBearer, Mechanism::XOauth2]
};
trc::event!(
Pop3(trc::Pop3Event::Capabilities),
SpanId = self.session_id,
Tls = self.stream.is_tls(),
Strict = !self.server.core.imap.allow_plain_auth,
Elapsed = trc::Value::Duration(0)
);
self.write_bytes(
Response::Capability::<u32> {
mechanisms,
stls: !self.stream.is_tls(),
}
.serialize(),
)
.await
}
pub async fn handle_stls(&mut self) -> trc::Result<()> {
trc::event!(
Pop3(trc::Pop3Event::StartTls),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("Begin TLS negotiation now").await
}
pub async fn handle_utf8(&mut self) -> trc::Result<()> {
trc::event!(
Pop3(trc::Pop3Event::Utf8),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("UTF8 enabled").await
}
}
+74
View File
@@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod request;
pub mod response;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Command<T, M> {
// Authorization state
User {
name: T,
},
Pass {
string: T,
},
Apop {
name: T,
digest: T,
},
Quit,
// Transaction state
Stat,
List {
msg: Option<u32>,
},
Retr {
msg: u32,
},
Dele {
msg: u32,
},
DeleMany {
msgs: Vec<u32>,
},
#[default]
Noop,
Rset,
Top {
msg: u32,
n: u32,
},
Uidl {
msg: Option<u32>,
},
// Extensions
Capa,
Stls,
Utf8,
Auth {
mechanism: M,
params: Vec<T>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mechanism {
Plain,
CramMd5,
DigestMd5,
ScramSha1,
ScramSha256,
Apop,
Ntlm,
Gssapi,
Anonymous,
External,
OAuthBearer,
XOauth2,
}
+455
View File
@@ -0,0 +1,455 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use super::{Command, Mechanism};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NeedsMoreData,
Parse(Cow<'static, str>),
}
#[derive(Default, Debug)]
pub enum State {
#[default]
Init,
Command {
buf: [u8; 4],
len: usize,
},
Argument {
request: Command<Vec<u8>, Vec<u8>>,
num: usize,
last_is_space: bool,
},
Error {
reason: Cow<'static, str>,
},
}
#[derive(Default)]
pub struct Parser {
pub state: State,
}
const MAX_ARG_LEN: usize = 256;
impl Parser {
pub fn parse(
&mut self,
bytes: &mut std::slice::Iter<'_, u8>,
) -> Result<Command<String, Mechanism>, Error> {
for &byte in bytes {
match &mut self.state {
State::Init => match byte {
b' ' | b'\t' | b'\r' | b'\n' => {}
b'a'..=b'z' => {
self.state = State::Command {
buf: [byte, 0, 0, 0],
len: 1,
};
}
b'A'..=b'Z' => {
self.state = State::Command {
buf: [byte | 0x20, 0, 0, 0],
len: 1,
};
}
_ => {
self.state = State::Error {
reason: "Invalid command".into(),
};
}
},
State::Command { buf, len } => match byte {
b'a'..=b'z' | b'8' if *len < 4 => {
buf[*len] = byte;
*len += 1;
}
b'A'..=b'Z' if *len < 4 => {
buf[*len] = byte | 0x20;
*len += 1;
}
b' ' | b'\t' if *len == 4 || *len == 3 => match Command::parse(buf) {
Ok(request) => {
self.state = State::Argument {
request,
num: 0,
last_is_space: true,
};
}
Err(err) => {
self.state = State::Error { reason: err };
}
},
b'\r' => {}
b'\n' if *len == 4 || *len == 3 => match Command::parse(buf) {
Ok(request) => {
self.state = State::Init;
return request.finalize(0);
}
Err(err) => {
self.state = State::Init;
return Err(Error::Parse(err));
}
},
_ => {
self.state = State::Error {
reason: "Invalid command".into(),
};
}
},
State::Argument {
request,
num,
last_is_space,
} => match byte {
b' ' | b'\t' => {
*last_is_space = true;
}
b'\r' => {}
b'\n' => {
let request = std::mem::take(request).finalize(*num);
self.state = State::Init;
return request;
}
_ => {
if *last_is_space {
*num += 1;
}
match request.update_argument(*num, byte) {
Ok(_) => {
*last_is_space = false;
}
Err(err) => {
self.state = State::Error { reason: err };
}
}
}
},
State::Error { reason } => {
if byte == b'\n' {
let reason = std::mem::take(reason);
self.state = State::Init;
return Err(Error::Parse(reason));
}
}
}
}
Err(Error::NeedsMoreData)
}
}
impl Command<Vec<u8>, Vec<u8>> {
pub fn parse(bytes: &[u8; 4]) -> Result<Self, Cow<'static, str>> {
match (bytes[0], bytes[1], bytes[2], bytes[3]) {
(b'u', b's', b'e', b'r') => Ok(Self::User { name: Vec::new() }),
(b'u', b'i', b'd', b'l') => Ok(Self::Uidl { msg: None }),
(b'u', b't', b'f', b'8') => Ok(Self::Utf8),
(b'p', b'a', b's', b's') => Ok(Self::Pass { string: Vec::new() }),
(b'a', b'p', b'o', b'p') => Ok(Self::Apop {
name: Vec::new(),
digest: Vec::new(),
}),
(b'a', b'u', b't', b'h') => Ok(Self::Auth {
mechanism: Vec::new(),
params: Vec::new(),
}),
(b'q', b'u', b'i', b't') => Ok(Self::Quit),
(b'l', b'i', b's', b't') => Ok(Self::List { msg: None }),
(b'r', b'e', b't', b'r') => Ok(Self::Retr { msg: 0 }),
(b'r', b's', b'e', b't') => Ok(Self::Rset),
(b'd', b'e', b'l', b'e') => Ok(Self::Dele { msg: 0 }),
(b'n', b'o', b'o', b'p') => Ok(Self::Noop),
(b't', b'o', b'p', 0) => Ok(Self::Top { msg: 0, n: 0 }),
(b'c', b'a', b'p', b'a') => Ok(Self::Capa),
(b's', b't', b'l', b's') => Ok(Self::Stls),
(b's', b't', b'a', b't') => Ok(Self::Stat),
_ => Err("Invalid command".into()),
}
}
pub fn update_argument(&mut self, arg_num: usize, byte: u8) -> Result<(), Cow<'static, str>> {
match self {
Command::User { name } if arg_num == 1 && name.len() < MAX_ARG_LEN => {
name.push(byte);
Ok(())
}
Command::Pass { string } if arg_num == 1 && string.len() < MAX_ARG_LEN => {
string.push(byte);
Ok(())
}
Command::Apop { name, digest }
if arg_num <= 2 && name.len() < MAX_ARG_LEN && digest.len() < MAX_ARG_LEN =>
{
if arg_num == 1 {
name.push(byte);
} else {
digest.push(byte);
}
Ok(())
}
Command::List { msg } if arg_num == 1 => add_digit(msg.get_or_insert(0), byte),
Command::Retr { msg } if arg_num == 1 => add_digit(msg, byte),
Command::Dele { msg } if arg_num == 1 => add_digit(msg, byte),
Command::Top { msg, n } if arg_num <= 2 => {
if arg_num == 1 {
add_digit(msg, byte)
} else {
add_digit(n, byte)
}
}
Command::Uidl { msg } if arg_num == 1 => add_digit(msg.get_or_insert(0), byte),
Command::Auth { mechanism, params }
if arg_num <= 4
&& mechanism.len() < 64
&& params.iter().map(|p| p.len()).sum::<usize>() < (MAX_ARG_LEN * 4) =>
{
if arg_num == 1 {
mechanism.push(byte);
} else {
if params.len() < arg_num - 1 {
params.push(Vec::new());
}
params.last_mut().unwrap().push(byte);
}
Ok(())
}
_ => Err("Too many arguments".into()),
}
}
pub fn finalize(self, num_args: usize) -> Result<Command<String, Mechanism>, Error> {
match self {
Command::User { name } if num_args == 1 => {
into_string(name).map(|name| Command::User { name })
}
Command::Pass { string } if num_args == 1 => {
into_string(string).map(|string| Command::Pass { string })
}
Command::Apop { name, digest } if num_args == 2 => {
let name = into_string(name)?;
let digest = into_string(digest)?;
Ok(Command::Apop { name, digest })
}
Command::Quit => Ok(Command::Quit),
Command::Stat => Ok(Command::Stat),
Command::List { msg } => Ok(Command::List { msg }),
Command::Retr { msg } if num_args == 1 => Ok(Command::Retr { msg }),
Command::Dele { msg } if num_args == 1 => Ok(Command::Dele { msg }),
Command::Noop => Ok(Command::Noop),
Command::Rset => Ok(Command::Rset),
Command::Top { msg, n } if num_args == 2 => Ok(Command::Top { msg, n }),
Command::Uidl { msg } => Ok(Command::Uidl { msg }),
Command::Capa => Ok(Command::Capa),
Command::Stls => Ok(Command::Stls),
Command::Utf8 => Ok(Command::Utf8),
Command::Auth { mechanism, params } if num_args >= 1 => {
let mechanism = Mechanism::parse(&mechanism)?;
let params = params
.into_iter()
.map(into_string)
.collect::<Result<_, _>>()?;
Ok(Command::Auth { mechanism, params })
}
_ => Err(Error::Parse("Missing arguments".into())),
}
}
}
#[inline(always)]
fn into_string(bytes: Vec<u8>) -> Result<String, Error> {
String::from_utf8(bytes).map_err(|_| Error::Parse("Invalid UTF-8".into()))
}
#[inline(always)]
fn add_digit(num: &mut u32, byte: u8) -> Result<(), Cow<'static, str>> {
if byte.is_ascii_digit() {
*num = num
.checked_mul(10)
.and_then(|n| n.checked_add((byte - b'0') as u32))
.ok_or("Numeric argument out of range")?;
Ok(())
} else {
Err("Invalid digit".into())
}
}
impl Mechanism {
pub fn parse(value: &[u8]) -> Result<Self, Error> {
if value.eq_ignore_ascii_case(b"PLAIN") {
Ok(Self::Plain)
} else if value.eq_ignore_ascii_case(b"CRAM-MD5") {
Ok(Self::CramMd5)
} else if value.eq_ignore_ascii_case(b"DIGEST-MD5") {
Ok(Self::DigestMd5)
} else if value.eq_ignore_ascii_case(b"SCRAM-SHA-1") {
Ok(Self::ScramSha1)
} else if value.eq_ignore_ascii_case(b"SCRAM-SHA-256") {
Ok(Self::ScramSha256)
} else if value.eq_ignore_ascii_case(b"APOP") {
Ok(Self::Apop)
} else if value.eq_ignore_ascii_case(b"NTLM") {
Ok(Self::Ntlm)
} else if value.eq_ignore_ascii_case(b"GSSAPI") {
Ok(Self::Gssapi)
} else if value.eq_ignore_ascii_case(b"ANONYMOUS") {
Ok(Self::Anonymous)
} else if value.eq_ignore_ascii_case(b"EXTERNAL") {
Ok(Self::External)
} else if value.eq_ignore_ascii_case(b"OAUTHBEARER") {
Ok(Self::OAuthBearer)
} else if value.eq_ignore_ascii_case(b"XOAUTH2") {
Ok(Self::XOauth2)
} else {
Err(Error::Parse(
format!(
"Unsupported mechanism '{}'.",
String::from_utf8_lossy(value)
)
.into(),
))
}
}
}
#[cfg(test)]
mod tests {
use crate::protocol::{Command, Mechanism, request::Error};
use super::Parser;
#[test]
fn parse_command() {
let mut parser = Parser::default();
let mut chunked = String::new();
let mut chunked_expected = Vec::new();
for (cmd, request) in [
("QuiT", Command::Quit),
(" \r\n NOOP ", Command::Noop),
("STAT ", Command::Stat),
("LIST ", Command::List { msg: None }),
(" list 100 ", Command::List { msg: 100.into() }),
("retr 55", Command::Retr { msg: 55 }),
("DELE 99", Command::Dele { msg: 99 }),
(" rset ", Command::Rset),
("top 8000 1234", Command::Top { msg: 8000, n: 1234 }),
("uidl", Command::Uidl { msg: None }),
("uidl 000099999", Command::Uidl { msg: 99999.into() }),
(
"USER test",
Command::User {
name: "test".to_string(),
},
),
(
"PASS secret",
Command::Pass {
string: "secret".to_string(),
},
),
(
"APOP mrose c4c9334bac560ecc979e58001b3e22fb",
Command::Apop {
name: "mrose".to_string(),
digest: "c4c9334bac560ecc979e58001b3e22fb".to_string(),
},
),
("utf8", Command::Utf8),
("capa", Command::Capa),
(
"AUTH GSSAPI",
Command::Auth {
mechanism: Mechanism::Gssapi,
params: vec![],
},
),
(
"AUTH PLAIN dGVzdAB0ZXN0AHRlc3Q=",
Command::Auth {
mechanism: Mechanism::Plain,
params: vec!["dGVzdAB0ZXN0AHRlc3Q=".to_string()],
},
),
] {
assert_eq!(
parser.parse(&mut cmd.as_bytes().iter()),
Err(Error::NeedsMoreData)
);
assert_eq!(
parser.parse(&mut b"\r\n".iter()),
Ok(request.clone()),
"{:?}",
cmd
);
chunked.push_str(cmd);
chunked.push_str("\r\n");
chunked_expected.push(request);
}
for chunk_size in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] {
let mut parser = Parser::default();
let mut requests = Vec::new();
for chunk in chunked.as_bytes().chunks(chunk_size) {
let mut chunk = chunk.iter();
loop {
match parser.parse(&mut chunk) {
Ok(request) => {
requests.push(request);
}
Err(Error::NeedsMoreData) => break,
Err(err) => {
panic!("Unexpected error on chunk size {chunk_size}: {err:?}");
}
}
}
}
assert_eq!(requests, chunked_expected, "Chunk size: {}", chunk_size);
}
for cmd in [
"user",
"pass",
"user a b",
"pass c d",
"apop",
"apop a",
"apop a b c",
"quit 1",
"stat 1",
"list 1 2",
"retr",
"retr 1 2",
"dele",
"dele 1 2",
"noop 1",
"rset 1",
"top",
"top 1 2 3",
"uidl 1 2 3",
"capa 1",
"stls 1",
"utf8 1",
"auth",
"auth unknown",
] {
assert_eq!(
parser.parse(&mut cmd.as_bytes().iter()),
Err(Error::NeedsMoreData)
);
let result = parser.parse(&mut b"\r\n".iter());
assert!(result.is_err(), "{:?}", result);
}
}
}
+217
View File
@@ -0,0 +1,217 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::Mechanism;
use std::{borrow::Cow, fmt::Display};
use utils::chained_bytes::SliceRange;
pub enum Response<'x, T> {
Ok(Cow<'static, str>),
Err(Cow<'static, str>),
List(Vec<T>),
Message {
bytes: SliceRange<'x>,
lines: u32,
},
Capability {
mechanisms: Vec<Mechanism>,
stls: bool,
},
}
impl<'x, T: Display> Response<'x, T> {
pub fn serialize(&self) -> Vec<u8> {
match self {
Response::Ok(message) => {
let mut buf = Vec::with_capacity(message.len() + 6);
buf.extend_from_slice(b"+OK ");
buf.extend_from_slice(message.as_bytes());
buf.extend_from_slice(b"\r\n");
buf
}
Response::Err(message) => {
let mut buf = Vec::with_capacity(message.len() + 6);
buf.extend_from_slice(b"-ERR ");
buf.extend_from_slice(message.as_bytes());
buf.extend_from_slice(b"\r\n");
buf
}
Response::List(octets) => {
let mut buf = Vec::with_capacity(octets.len() * 8 + 10);
buf.extend_from_slice(format!("+OK {} messages\r\n", octets.len()).as_bytes());
for (num, octet) in octets.iter().enumerate() {
buf.extend_from_slice((num + 1).to_string().as_bytes());
buf.extend_from_slice(b" ");
buf.extend_from_slice(octet.to_string().as_bytes());
buf.extend_from_slice(b"\r\n");
}
buf.extend_from_slice(b".\r\n");
buf
}
Response::Message { bytes, lines } => {
let mut buf = Vec::with_capacity(bytes.len() + 10);
buf.extend_from_slice(b"+OK ");
buf.extend_from_slice(bytes.len().to_string().as_bytes());
buf.extend_from_slice(b" octets\r\n");
let mut line_count = 0;
let mut last_byte = 0;
// Transparency procedure
for &byte in bytes.into_iter() {
// POP3 requires that lines end with CRLF, do this check to ensure that
if byte == b'\n' && last_byte != b'\r' {
buf.push(b'\r');
}
if byte == b'.' && last_byte == b'\n' {
buf.push(b'.');
}
buf.push(byte);
last_byte = byte;
if *lines > 0 && byte == b'\n' {
line_count += 1;
if line_count == *lines {
break;
}
}
}
if last_byte != b'\n' {
buf.extend_from_slice(b"\r\n");
}
buf.extend_from_slice(b".\r\n");
buf
}
Response::Capability { mechanisms, stls } => {
let mut buf = Vec::with_capacity(256);
buf.extend_from_slice(b"+OK Capability list follows\r\n");
if !mechanisms.is_empty() {
if mechanisms.contains(&Mechanism::Plain) {
buf.extend_from_slice(b"USER\r\n");
}
buf.extend_from_slice(b"SASL");
for mechanism in mechanisms {
buf.extend_from_slice(b" ");
buf.extend_from_slice(mechanism.as_str().as_bytes());
}
buf.extend_from_slice(b"\r\n");
}
if *stls {
buf.extend_from_slice(b"STLS\r\n");
}
for capa in [
"TOP",
"RESP-CODES",
"PIPELINING",
"EXPIRE NEVER",
"UIDL",
"UTF8",
"IMPLEMENTATION Stalwart Server",
] {
buf.extend_from_slice(capa.as_bytes());
buf.extend_from_slice(b"\r\n");
}
buf.extend_from_slice(b".\r\n");
buf
}
}
}
}
impl Mechanism {
pub fn as_str(&self) -> &'static str {
match self {
Mechanism::Plain => "PLAIN",
Mechanism::CramMd5 => "CRAM-MD5",
Mechanism::DigestMd5 => "DIGEST-MD5",
Mechanism::ScramSha1 => "SCRAM-SHA-1",
Mechanism::ScramSha256 => "SCRAM-SHA-256",
Mechanism::Apop => "APOP",
Mechanism::Ntlm => "NTLM",
Mechanism::Gssapi => "GSSAPI",
Mechanism::Anonymous => "ANONYMOUS",
Mechanism::External => "EXTERNAL",
Mechanism::OAuthBearer => "OAUTHBEARER",
Mechanism::XOauth2 => "XOAUTH2",
}
}
}
pub trait SerializeResponse {
fn serialize(&self) -> Vec<u8>;
}
impl SerializeResponse for trc::Error {
fn serialize(&self) -> Vec<u8> {
let message = self
.value_as_str(trc::Key::Details)
.unwrap_or_else(|| self.as_ref().message());
let mut buf = Vec::with_capacity(message.len() + 6);
buf.extend_from_slice(b"-ERR ");
buf.extend_from_slice(message.as_bytes());
buf.extend_from_slice(b"\r\n");
buf
}
}
#[cfg(test)]
mod tests {
use super::Response;
use crate::protocol::Mechanism;
use utils::chained_bytes::SliceRange;
#[test]
fn serialize_response() {
for (cmd, expected) in [
(
Response::Ok("message 1 deleted".into()),
"+OK message 1 deleted\r\n",
),
(
Response::Err("permission denied".into()),
"-ERR permission denied\r\n",
),
(
Response::List(vec![100, 200, 300]),
"+OK 3 messages\r\n1 100\r\n2 200\r\n3 300\r\n.\r\n",
),
(
Response::Capability {
mechanisms: vec![Mechanism::Plain, Mechanism::CramMd5],
stls: true,
},
concat!(
"+OK Capability list follows\r\n",
"USER\r\n",
"SASL PLAIN CRAM-MD5\r\n",
"STLS\r\n",
"TOP\r\n",
"RESP-CODES\r\n",
"PIPELINING\r\n",
"EXPIRE NEVER\r\n",
"UIDL\r\n",
"UTF8\r\n",
"IMPLEMENTATION Stalwart Server\r\n.\r\n"
),
),
(
Response::Message {
bytes: SliceRange::Split(b"Subject: test\r\n\r\n.\r\n", b"test.\r\n.test\r\na"),
lines: 0,
},
"+OK 35 octets\r\nSubject: test\r\n\r\n..\r\ntest.\r\n..test\r\na\r\n.\r\n",
),
] {
assert_eq!(expected, String::from_utf8(cmd.serialize()).unwrap());
}
}
}
+196
View File
@@ -0,0 +1,196 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Pop3SessionManager, SERVER_GREETING, Session, State,
protocol::{
request::Parser,
response::{Response, SerializeResponse},
},
};
use common::{
BuildServer,
network::{SessionData, SessionManager, SessionResult, SessionStream},
};
use std::borrow::Cow;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_rustls::server::TlsStream;
impl SessionManager for Pop3SessionManager {
#[allow(clippy::manual_async_fn)]
fn handle<T: SessionStream>(
self,
session: SessionData<T>,
) -> impl std::future::Future<Output = ()> + Send {
async move {
let mut session = Session {
server: self.inner.build_server(),
instance: session.instance,
receiver: Parser::default(),
state: State::NotAuthenticated {
auth_failures: 0,
username: None,
},
stream: session.stream,
in_flight: session.in_flight,
remote_addr: session.remote_ip,
session_id: session.session_id,
};
if session
.write_bytes(SERVER_GREETING.as_bytes())
.await
.is_ok()
&& session.handle_conn().await
&& session.instance.acceptor.is_tls()
&& let Ok(mut session) = session.into_tls().await
{
session.handle_conn().await;
}
}
}
#[allow(clippy::manual_async_fn)]
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
async {}
}
}
impl<T: SessionStream> Session<T> {
pub async fn handle_conn(&mut self) -> bool {
let mut buf = vec![0; 8192];
let mut shutdown_rx = self.instance.shutdown_rx.clone();
loop {
tokio::select! {
result = tokio::time::timeout(
if !matches!(self.state, State::NotAuthenticated {..}) {
self.server.core.imap.timeout_auth
} else {
self.server.core.imap.timeout_unauth
},
self.stream.read(&mut buf)) => {
match result {
Ok(Ok(bytes_read)) => {
if bytes_read > 0 {
match self.ingest(&buf[..bytes_read]).await {
SessionResult::Continue => (),
SessionResult::UpgradeTls => {
return true;
}
SessionResult::Close => {
break;
}
}
} else {
trc::event!(
Network(trc::NetworkEvent::Closed),
SpanId = self.session_id,
CausedBy = trc::location!()
);
break;
}
},
Ok(Err(err)) => {
trc::event!(
Network(trc::NetworkEvent::ReadError),
SpanId = self.session_id,
Reason = err.to_string() ,
CausedBy = trc::location!()
);
break;
},
Err(_) => {
trc::event!(
Network(trc::NetworkEvent::Timeout),
SpanId = self.session_id,
CausedBy = trc::location!()
);
self.write_bytes(&b"-ERR Connection timed out.\r\n"[..]).await.ok();
break;
}
}
},
_ = shutdown_rx.changed() => {
trc::event!(
Network(trc::NetworkEvent::Closed),
SpanId = self.session_id,
Reason = "Server shutting down",
CausedBy = trc::location!()
);
self.write_bytes(&b"* BYE Server shutting down.\r\n"[..]).await.ok();
break;
}
};
}
false
}
pub async fn into_tls(self) -> Result<Session<TlsStream<T>>, ()> {
Ok(Session {
stream: self
.instance
.tls_accept(self.stream, self.session_id)
.await?,
server: self.server,
instance: self.instance,
receiver: Parser::default(),
state: self.state,
session_id: self.session_id,
in_flight: self.in_flight,
remote_addr: self.remote_addr,
})
}
}
impl<T: SessionStream> Session<T> {
pub async fn write_bytes(&mut self, bytes: impl AsRef<[u8]>) -> trc::Result<()> {
let bytes = bytes.as_ref();
trc::event!(
Pop3(trc::Pop3Event::RawOutput),
SpanId = self.session_id,
Size = bytes.len(),
Contents = trc::Value::from_maybe_string(bytes),
);
self.stream.write_all(bytes.as_ref()).await.map_err(|err| {
trc::NetworkEvent::WriteError
.into_err()
.reason(err)
.caused_by(trc::location!())
})?;
self.stream.flush().await.map_err(|err| {
trc::NetworkEvent::WriteError
.into_err()
.reason(err)
.caused_by(trc::location!())
})
}
pub async fn write_ok(&mut self, message: impl Into<Cow<'static, str>>) -> trc::Result<()> {
self.write_bytes(Response::Ok::<u32>(message.into()).serialize())
.await
}
pub async fn write_err(&mut self, err: trc::Error) -> bool {
let disconnect = err.must_disconnect();
let response = err.serialize();
let write_err = err.should_write_err();
trc::error!(err.span_id(self.session_id));
if write_err && let Err(err) = self.write_bytes(response).await {
trc::error!(err.span_id(self.session_id));
return false;
}
!disconnect
}
}