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:
@@ -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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user