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,519 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{iter::Peekable, sync::Arc, vec::IntoIter};
|
||||
|
||||
use common::{
|
||||
KV_RATE_LIMIT_IMAP,
|
||||
network::{SessionResult, SessionStream},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse,
|
||||
receiver::{self, Request},
|
||||
};
|
||||
use trc::SecurityEvent;
|
||||
|
||||
use super::{SelectedMailbox, Session, SessionData, State};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult {
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::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);
|
||||
let mut needs_literal = None;
|
||||
let mut has_expunge = false;
|
||||
|
||||
loop {
|
||||
match self.receiver.parse(&mut bytes) {
|
||||
Ok(request) => match self.is_allowed(request).await {
|
||||
Ok(request) => {
|
||||
has_expunge |=
|
||||
matches!(request.command, Command::Expunge(_) | Command::Close);
|
||||
requests.push(request);
|
||||
}
|
||||
Err(err) => {
|
||||
if !self.write_error(err).await {
|
||||
return SessionResult::Close;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(receiver::Error::NeedsMoreData) => {
|
||||
break;
|
||||
}
|
||||
Err(receiver::Error::NeedsLiteral { size }) => {
|
||||
needs_literal = size.into();
|
||||
break;
|
||||
}
|
||||
Err(receiver::Error::Error { response }) => {
|
||||
// Check for port scanners
|
||||
if matches!(
|
||||
(&self.state, response.key(trc::Key::Code)),
|
||||
(
|
||||
State::NotAuthenticated { .. },
|
||||
Some(trc::Value::String(v))
|
||||
) if v == "PARSE"
|
||||
) {
|
||||
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 IMAP command",
|
||||
);
|
||||
|
||||
return SessionResult::Close;
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.span_id(self.session_id)
|
||||
.details("Failed to check for fail2ban")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.write_error(response).await {
|
||||
return SessionResult::Close;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut requests = requests.into_iter().peekable();
|
||||
while let Some(request) = requests.next() {
|
||||
let result = match request.command {
|
||||
Command::List | Command::Lsub => self
|
||||
.handle_list(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Select | Command::Examine => self
|
||||
.handle_select(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Create => self
|
||||
.handle_create(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Delete => self
|
||||
.handle_delete(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Rename => self
|
||||
.handle_rename(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Status => self
|
||||
.handle_status(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Append => self
|
||||
.handle_append(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Close => self
|
||||
.handle_close(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Unselect => self
|
||||
.handle_unselect(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Expunge(is_uid) => self
|
||||
.handle_expunge(request, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Search(is_uid) => self
|
||||
.handle_search(request, false, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Fetch(_) => self
|
||||
.handle_fetch(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Store(is_uid) => self
|
||||
.handle_store(request, is_uid, !has_expunge)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Copy(is_uid) => self
|
||||
.handle_copy_move(request, false, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Move(is_uid) => self
|
||||
.handle_copy_move(request, true, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Sort(is_uid) => self
|
||||
.handle_search(request, true, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Thread(is_uid) => self
|
||||
.handle_thread(request, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Idle => self
|
||||
.handle_idle(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Subscribe => self
|
||||
.handle_subscribe(request, true)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Unsubscribe => self
|
||||
.handle_subscribe(request, false)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Namespace => self
|
||||
.handle_namespace(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Authenticate => Box::pin(self.handle_authenticate(request))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Login => Box::pin(self.handle_login(request))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Capability => self
|
||||
.handle_capability(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Enable => self
|
||||
.handle_enable(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::StartTls => self
|
||||
.write_bytes(
|
||||
StatusResponse::ok("Begin TLS negotiation now")
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
.map(|_| SessionResult::UpgradeTls),
|
||||
Command::Noop => self
|
||||
.handle_noop(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Check => self
|
||||
.handle_noop(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Logout => self
|
||||
.handle_logout(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Close),
|
||||
Command::SetAcl => self
|
||||
.handle_set_acl(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::DeleteAcl => self
|
||||
.handle_set_acl(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetAcl => self
|
||||
.handle_get_acl(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::ListRights => self
|
||||
.handle_list_rights(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::MyRights => self
|
||||
.handle_my_rights(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetQuota => self
|
||||
.handle_get_quota(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetQuotaRoot => self
|
||||
.handle_get_quota_root(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Unauthenticate => self
|
||||
.handle_unauthenticate(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Id => self
|
||||
.handle_id(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetJmapAccess => self
|
||||
.handle_jmap_access(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::UidBatches => self
|
||||
.handle_uidbatches(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(SessionResult::Continue) => (),
|
||||
Ok(result) => return result,
|
||||
Err(err) => {
|
||||
if !self.write_error(err).await {
|
||||
return SessionResult::Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(needs_literal) = needs_literal
|
||||
&& let Err(err) = self
|
||||
.write_bytes(format!("+ Ready for {} bytes.\r\n", needs_literal).into_bytes())
|
||||
.await
|
||||
{
|
||||
self.write_error(err).await;
|
||||
return SessionResult::Close;
|
||||
}
|
||||
|
||||
SessionResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group_requests(
|
||||
requests: &mut Peekable<IntoIter<Request<Command>>>,
|
||||
mut grouped_requests: Vec<Request<Command>>,
|
||||
) -> Vec<Request<Command>> {
|
||||
let last_command = grouped_requests.last().unwrap().command;
|
||||
loop {
|
||||
match requests.peek() {
|
||||
Some(request) if request.command == last_command => {
|
||||
grouped_requests.push(requests.next().unwrap());
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
grouped_requests
|
||||
}
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
async fn is_allowed(&self, request: Request<Command>) -> trc::Result<Request<Command>> {
|
||||
let state = &self.state;
|
||||
// Rate limit request
|
||||
if let State::Authenticated { data } | State::Selected { data, .. } = state
|
||||
&& let Some(rate) = &self.server.core.imap.rate_requests
|
||||
&& data
|
||||
.server
|
||||
.in_memory_store()
|
||||
.is_rate_allowed(
|
||||
KV_RATE_LIMIT_IMAP,
|
||||
&data.account_id.to_be_bytes(),
|
||||
rate,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(trc::LimitEvent::TooManyRequests.into_err());
|
||||
}
|
||||
|
||||
match &request.command {
|
||||
Command::Capability | Command::Noop | Command::Logout | Command::Id => Ok(request),
|
||||
Command::StartTls => {
|
||||
if !self.is_tls {
|
||||
if self.instance.acceptor.is_tls() {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("TLS is not available.")
|
||||
.id(request.tag))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Already in TLS mode.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Authenticate => {
|
||||
if let State::NotAuthenticated { .. } = state {
|
||||
if self.is_tls || self.server.core.imap.allow_plain_auth {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Cannot authenticate over plain-text.")
|
||||
.code(ResponseCode::PrivacyRequired)
|
||||
.id(request.tag))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Already authenticated.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Login => {
|
||||
if let State::NotAuthenticated { .. } = state {
|
||||
if self.is_tls || self.server.core.imap.allow_plain_auth {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("LOGIN is disabled on the clear-text port.")
|
||||
.id(request.tag))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Already authenticated.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Enable
|
||||
| Command::Select
|
||||
| Command::Examine
|
||||
| Command::Create
|
||||
| Command::Delete
|
||||
| Command::Rename
|
||||
| Command::Subscribe
|
||||
| Command::Unsubscribe
|
||||
| Command::List
|
||||
| Command::Lsub
|
||||
| Command::Namespace
|
||||
| Command::Status
|
||||
| Command::Append
|
||||
| Command::Idle
|
||||
| Command::SetAcl
|
||||
| Command::DeleteAcl
|
||||
| Command::GetAcl
|
||||
| Command::ListRights
|
||||
| Command::MyRights
|
||||
| Command::Unauthenticate
|
||||
| Command::GetQuota
|
||||
| Command::GetQuotaRoot
|
||||
| Command::GetJmapAccess => {
|
||||
if let State::Authenticated { .. } | State::Selected { .. } = state {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Not authenticated.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Close
|
||||
| Command::Unselect
|
||||
| Command::Expunge(_)
|
||||
| Command::Search(_)
|
||||
| Command::Fetch(_)
|
||||
| Command::Store(_)
|
||||
| Command::Copy(_)
|
||||
| Command::Move(_)
|
||||
| Command::Check
|
||||
| Command::Sort(_)
|
||||
| Command::Thread(_)
|
||||
| Command::UidBatches => match state {
|
||||
State::Selected { mailbox, .. } => {
|
||||
// RFC 9586 forbids message numbers once UIDONLY is enabled
|
||||
if self.is_uidonly && request.command.requires_uid() {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Message numbers are not allowed once UIDONLY is enabled.")
|
||||
.code(ResponseCode::UidRequired)
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(request.tag))
|
||||
} else if mailbox.is_select
|
||||
|| !matches!(
|
||||
request.command,
|
||||
Command::Store(_) | Command::Expunge(_) | Command::Move(_),
|
||||
)
|
||||
{
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Not permitted in EXAMINE state.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
State::Authenticated { .. } => Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("No mailbox is selected.")
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(request.tag)),
|
||||
State::NotAuthenticated { .. } => Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Not authenticated.")
|
||||
.id(request.tag)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> State<T> {
|
||||
pub fn auth_failures(&self) -> u32 {
|
||||
match self {
|
||||
State::NotAuthenticated { auth_failures, .. } => *auth_failures,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_data(&self) -> Arc<SessionData<T>> {
|
||||
match self {
|
||||
State::Authenticated { data } => data.clone(),
|
||||
State::Selected { data, .. } => data.clone(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mailbox_state(&self) -> (Arc<SessionData<T>>, Arc<SelectedMailbox>) {
|
||||
match self {
|
||||
State::Selected { data, mailbox, .. } => (data.clone(), mailbox.clone()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_mailbox_state(&self) -> (Arc<SessionData<T>>, Option<Arc<SelectedMailbox>>) {
|
||||
match self {
|
||||
State::Authenticated { data } => (data.clone(), None),
|
||||
State::Selected { data, mailbox, .. } => (data.clone(), mailbox.clone().into()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_data(&self) -> (Arc<SessionData<T>>, Arc<SelectedMailbox>) {
|
||||
match self {
|
||||
State::Selected { data, mailbox } => (data.clone(), mailbox.clone()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_task<F, R, P>(&self, params: P, fnc: F) -> trc::Result<()>
|
||||
where
|
||||
F: FnOnce(P, &super::SessionData<T>) -> R + Send + 'static,
|
||||
P: Send + Sync + 'static,
|
||||
R: std::future::Future<Output = trc::Result<()>> + Send + 'static,
|
||||
{
|
||||
let data = self.session_data();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = fnc(params, &data).await {
|
||||
let _ = data.write_error(err).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
matches!(self, State::Authenticated { .. } | State::Selected { .. })
|
||||
}
|
||||
|
||||
pub fn close_mailbox(&self) -> bool {
|
||||
matches!(self, State::Selected { .. })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Account, MailboxId, MailboxSync, Session, SessionData};
|
||||
use crate::core::Mailbox;
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
network::{SessionStream, limiter::InFlight},
|
||||
sharing::EffectiveAcl,
|
||||
};
|
||||
use email::{
|
||||
cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess},
|
||||
mailbox::INBOX_ID,
|
||||
};
|
||||
use imap_proto::protocol::list::Attribute;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::BTreeMap, sync::atomic::Ordering};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::Collection, keyword::Keyword, special_use::SpecialUse};
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn new(
|
||||
session: &Session<T>,
|
||||
access_token: AccessToken,
|
||||
in_flight: Option<InFlight>,
|
||||
) -> trc::Result<Self> {
|
||||
let mut session = SessionData {
|
||||
stream_tx: session.stream_tx.clone(),
|
||||
server: session.server.clone(),
|
||||
account_id: access_token.account_id(),
|
||||
session_id: session.session_id,
|
||||
mailboxes: Mutex::new(vec![]),
|
||||
state: access_token.state().into(),
|
||||
remote_addr: session.remote_addr,
|
||||
access_token,
|
||||
in_flight,
|
||||
};
|
||||
|
||||
// Fetch mailboxes for the main account
|
||||
let mut mailboxes = vec![
|
||||
session
|
||||
.fetch_account_mailboxes(session.account_id, None, &session.access_token, None)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap(),
|
||||
];
|
||||
|
||||
// Fetch shared mailboxes
|
||||
for &account_id in session.access_token.shared_accounts(Collection::Mailbox) {
|
||||
let prefix: String = format!(
|
||||
"{}/{}",
|
||||
session.server.core.email.shared_folder,
|
||||
session
|
||||
.server
|
||||
.account(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.name()
|
||||
);
|
||||
mailboxes.push(
|
||||
session
|
||||
.fetch_account_mailboxes(account_id, prefix.into(), &session.access_token, None)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
session.mailboxes = Mutex::new(mailboxes);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
async fn fetch_account_mailboxes(
|
||||
&self,
|
||||
account_id: u32,
|
||||
mailbox_prefix: Option<String>,
|
||||
access_token: &AccessToken,
|
||||
current_state: Option<u64>,
|
||||
) -> trc::Result<Option<Account>> {
|
||||
let cache = self
|
||||
.server
|
||||
.get_cached_messages(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if current_state.is_some_and(|state| state == cache.last_change_id) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let shared_mailbox_ids = if access_token.is_member(account_id) {
|
||||
None
|
||||
} else {
|
||||
cache.shared_mailboxes(access_token, Acl::Read).into()
|
||||
};
|
||||
|
||||
// Build special uses
|
||||
let mut special_uses = AHashMap::new();
|
||||
for mailbox in &cache.mailboxes.items {
|
||||
if shared_mailbox_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(mailbox.document_id))
|
||||
&& !matches!(mailbox.role, SpecialUse::None)
|
||||
{
|
||||
special_uses.insert(mailbox.role, mailbox.document_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Build account
|
||||
let mut account = Account {
|
||||
account_id,
|
||||
prefix: mailbox_prefix,
|
||||
mailbox_names: BTreeMap::new(),
|
||||
mailbox_state: AHashMap::with_capacity(cache.mailboxes.items.len()),
|
||||
last_change_id: cache.last_change_id,
|
||||
};
|
||||
|
||||
for mailbox in &cache.mailboxes.items {
|
||||
if shared_mailbox_ids
|
||||
.as_ref()
|
||||
.is_some_and(|ids| !ids.contains(mailbox.document_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build mailbox path and map it to its effective id
|
||||
let mailbox_name = if let Some(prefix) = &account.prefix {
|
||||
let mut name = String::with_capacity(prefix.len() + mailbox.path.len() + 1);
|
||||
name.push_str(prefix.as_str());
|
||||
name.push('/');
|
||||
name.push_str(mailbox.path.as_str());
|
||||
name
|
||||
} else {
|
||||
mailbox.path.clone()
|
||||
};
|
||||
let effective_mailbox_id = self
|
||||
.server
|
||||
.core
|
||||
.email
|
||||
.default_folders
|
||||
.iter()
|
||||
.find(|f| f.name == mailbox_name || f.aliases.iter().any(|a| a == &mailbox_name))
|
||||
.and_then(|f| special_uses.get(&f.special_use))
|
||||
.copied()
|
||||
.unwrap_or(mailbox.document_id);
|
||||
account
|
||||
.mailbox_names
|
||||
.insert(mailbox_name, effective_mailbox_id);
|
||||
account.mailbox_state.insert(
|
||||
mailbox.document_id,
|
||||
Mailbox {
|
||||
has_children: cache
|
||||
.mailboxes
|
||||
.items
|
||||
.iter()
|
||||
.any(|child| child.parent_id == mailbox.document_id),
|
||||
is_subscribed: mailbox.subscribers.contains(&access_token.account_id()),
|
||||
special_use: match mailbox.role {
|
||||
SpecialUse::Trash => Some(Attribute::Trash),
|
||||
SpecialUse::Junk => Some(Attribute::Junk),
|
||||
SpecialUse::Drafts => Some(Attribute::Drafts),
|
||||
SpecialUse::Archive => Some(Attribute::Archive),
|
||||
SpecialUse::Sent => Some(Attribute::Sent),
|
||||
SpecialUse::Important => Some(Attribute::Important),
|
||||
SpecialUse::Memos => Some(Attribute::Memos),
|
||||
SpecialUse::Scheduled => Some(Attribute::Scheduled),
|
||||
SpecialUse::Snoozed => Some(Attribute::Snoozed),
|
||||
_ => None,
|
||||
},
|
||||
total_messages: cache.in_mailbox(mailbox.document_id).count() as u64,
|
||||
total_unseen: cache
|
||||
.in_mailbox_without_keyword(mailbox.document_id, &Keyword::Seen)
|
||||
.count() as u64,
|
||||
total_deleted: cache
|
||||
.in_mailbox_with_keyword(mailbox.document_id, &Keyword::Deleted)
|
||||
.count() as u64,
|
||||
uid_validity: mailbox.uid_validity as u64,
|
||||
uid_next: self
|
||||
.get_uid_next(&MailboxId {
|
||||
account_id,
|
||||
mailbox_id: mailbox.document_id,
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())? as u64,
|
||||
total_deleted_storage: None,
|
||||
size: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(account.into())
|
||||
}
|
||||
|
||||
pub async fn synchronize_mailboxes(
|
||||
&self,
|
||||
return_changes: bool,
|
||||
) -> trc::Result<Option<MailboxSync>> {
|
||||
let mut changes = if return_changes {
|
||||
MailboxSync::default().into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Obtain access token
|
||||
let access_token = self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let state = access_token.state();
|
||||
|
||||
// Shared mailboxes might have changed
|
||||
let mut added_accounts = Vec::new();
|
||||
if self.state.load(Ordering::Relaxed) != state {
|
||||
// Remove unlinked shared accounts
|
||||
let mut added_account_ids = Vec::new();
|
||||
{
|
||||
let mut mailboxes = self.mailboxes.lock();
|
||||
let mut new_accounts = Vec::with_capacity(mailboxes.len());
|
||||
let has_access_to = access_token
|
||||
.shared_accounts(Collection::Mailbox)
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
for account in mailboxes.drain(..) {
|
||||
if access_token.is_account_id(account.account_id)
|
||||
|| has_access_to.contains(&account.account_id)
|
||||
{
|
||||
new_accounts.push(account);
|
||||
} else {
|
||||
// Add unshared mailboxes to deleted list
|
||||
if let Some(changes) = &mut changes {
|
||||
for (mailbox_name, _) in account.mailbox_names {
|
||||
changes.deleted.push(mailbox_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add new shared account ids
|
||||
for account_id in has_access_to {
|
||||
if !new_accounts
|
||||
.iter()
|
||||
.skip(1)
|
||||
.any(|m| m.account_id == account_id)
|
||||
{
|
||||
added_account_ids.push(account_id);
|
||||
}
|
||||
}
|
||||
*mailboxes = new_accounts;
|
||||
}
|
||||
|
||||
// Fetch mailboxes for each new shared account
|
||||
for account_id in added_account_ids {
|
||||
let prefix: String = format!(
|
||||
"{}/{}",
|
||||
self.server.core.email.shared_folder,
|
||||
self.server
|
||||
.account(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.name()
|
||||
);
|
||||
added_accounts.push(
|
||||
self.fetch_account_mailboxes(account_id, prefix.into(), &access_token, None)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Update state
|
||||
self.state.store(state, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Fetch mailbox changes for all accounts
|
||||
let mut changed_accounts = Vec::new();
|
||||
let account_states = self
|
||||
.mailboxes
|
||||
.lock()
|
||||
.iter()
|
||||
.map(|m| (m.account_id, m.prefix.clone(), m.last_change_id))
|
||||
.collect::<Vec<_>>();
|
||||
for (account_id, prefix, last_state) in account_states {
|
||||
if let Some(changed_account) = self
|
||||
.fetch_account_mailboxes(account_id, prefix, &access_token, last_state.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
changed_accounts.push(changed_account);
|
||||
}
|
||||
}
|
||||
|
||||
// Update mailboxes
|
||||
if !changed_accounts.is_empty() || !added_accounts.is_empty() {
|
||||
let mut mailboxes = self.mailboxes.lock();
|
||||
|
||||
for changed_account in changed_accounts {
|
||||
if let Some(pos) = mailboxes
|
||||
.iter()
|
||||
.position(|a| a.account_id == changed_account.account_id)
|
||||
{
|
||||
// Add changes and deletions
|
||||
if let Some(changes) = &mut changes {
|
||||
let old_account = &mailboxes[pos];
|
||||
let new_account = &changed_account;
|
||||
|
||||
// Add new mailboxes
|
||||
for (mailbox_name, mailbox_id) in new_account.mailbox_names.iter() {
|
||||
if let Some(old_mailbox) = old_account.mailbox_state.get(mailbox_id) {
|
||||
if let Some(mailbox) = new_account.mailbox_state.get(mailbox_id)
|
||||
&& (mailbox.total_messages != old_mailbox.total_messages
|
||||
|| mailbox.total_unseen != old_mailbox.total_unseen)
|
||||
{
|
||||
changes.changed.push(mailbox_name.clone());
|
||||
}
|
||||
} else {
|
||||
changes.added.push(mailbox_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Add deleted mailboxes
|
||||
for (mailbox_name, mailbox_id) in &old_account.mailbox_names {
|
||||
if !new_account.mailbox_state.contains_key(mailbox_id) {
|
||||
changes.deleted.push(mailbox_name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mailboxes[pos] = changed_account;
|
||||
} else {
|
||||
// Add newly shared accounts
|
||||
if let Some(changes) = &mut changes {
|
||||
changes
|
||||
.added
|
||||
.extend(changed_account.mailbox_names.keys().cloned());
|
||||
}
|
||||
|
||||
mailboxes.push(changed_account);
|
||||
}
|
||||
}
|
||||
|
||||
if !added_accounts.is_empty() {
|
||||
// Add newly shared accounts
|
||||
if let Some(changes) = &mut changes {
|
||||
for added_account in &added_accounts {
|
||||
changes
|
||||
.added
|
||||
.extend(added_account.mailbox_names.keys().cloned());
|
||||
}
|
||||
}
|
||||
mailboxes.extend(added_accounts);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub fn get_mailbox_by_name(&self, mailbox_name: &str) -> Option<MailboxId> {
|
||||
let is_inbox = mailbox_name.eq_ignore_ascii_case("inbox");
|
||||
for account in self.mailboxes.lock().iter() {
|
||||
if account
|
||||
.prefix
|
||||
.as_ref()
|
||||
.is_none_or(|p| mailbox_name.starts_with(p.as_str()))
|
||||
{
|
||||
for (mailbox_name_, mailbox_id_) in account.mailbox_names.iter() {
|
||||
if (!is_inbox && mailbox_name_ == mailbox_name)
|
||||
|| (is_inbox && *mailbox_id_ == INBOX_ID)
|
||||
{
|
||||
return MailboxId {
|
||||
account_id: account.account_id,
|
||||
mailbox_id: *mailbox_id_,
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_mailbox_by_id(&self, account_id: u32, mailbox_id: u32) -> Option<MailboxId> {
|
||||
for account in self.mailboxes.lock().iter() {
|
||||
if account.account_id == account_id
|
||||
&& account.mailbox_names.values().any(|id| *id == mailbox_id)
|
||||
{
|
||||
return MailboxId {
|
||||
account_id,
|
||||
mailbox_id,
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn check_mailbox_acl(
|
||||
&self,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
item: Acl,
|
||||
) -> trc::Result<bool> {
|
||||
let access_token = self.refresh_access_token().await?;
|
||||
Ok(access_token.is_member(account_id)
|
||||
|| self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Mailbox,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.and_then(|mailbox| {
|
||||
if let Some(mailbox) = mailbox {
|
||||
Ok(Some(
|
||||
mailbox
|
||||
.unarchive::<email::mailbox::Mailbox>()?
|
||||
.acls
|
||||
.effective_acl(&access_token)
|
||||
.contains(item),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.caused_by(trc::location!())
|
||||
.details("Mailbox no longer exists.")
|
||||
})?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
ImapUidToId, Mailbox, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData,
|
||||
};
|
||||
use crate::core::ImapId;
|
||||
use ahash::AHashMap;
|
||||
use common::network::SessionStream;
|
||||
use email::cache::MessageCacheFetch;
|
||||
use imap_proto::protocol::{Sequence, expunge, select::Exists};
|
||||
use std::collections::BTreeMap;
|
||||
use store::{ValueKey, roaring::RoaringBitmap, write::ValueClass};
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::MailboxField};
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn fetch_messages(
|
||||
&self,
|
||||
mailbox: &MailboxId,
|
||||
current_state: Option<u64>,
|
||||
) -> trc::Result<Option<MailboxState>> {
|
||||
let cached_messages = self
|
||||
.server
|
||||
.get_cached_messages(mailbox.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if current_state.is_some_and(|state| state == cached_messages.emails.change_id) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Obtain UID next and assign UIDs
|
||||
let uid_map = cached_messages
|
||||
.emails
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
item.mailboxes.iter().find_map(|m| {
|
||||
if m.mailbox_id == mailbox.mailbox_id {
|
||||
Some((m.uid, item.document_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<BTreeMap<u32, u32>>();
|
||||
let mut uid_max = 0;
|
||||
let mut id_to_imap = AHashMap::with_capacity(uid_map.len());
|
||||
let mut uid_to_id = AHashMap::with_capacity(uid_map.len());
|
||||
|
||||
for (seqnum, (uid, message_id)) in uid_map.into_iter().enumerate() {
|
||||
if uid > uid_max {
|
||||
uid_max = uid;
|
||||
}
|
||||
id_to_imap.insert(
|
||||
message_id,
|
||||
ImapId {
|
||||
uid,
|
||||
seqnum: seqnum as u32 + 1,
|
||||
},
|
||||
);
|
||||
uid_to_id.insert(uid, message_id);
|
||||
}
|
||||
|
||||
Ok(Some(MailboxState {
|
||||
total_messages: id_to_imap.len(),
|
||||
id_to_imap,
|
||||
uid_to_id,
|
||||
uid_max,
|
||||
modseq: cached_messages.emails.change_id,
|
||||
next_state: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn synchronize_messages(&self, mailbox: &SelectedMailbox) -> trc::Result<u64> {
|
||||
// Obtain current modseq
|
||||
let mut current_modseq = mailbox.state.lock().modseq;
|
||||
if let Some(new_state) = self
|
||||
.fetch_messages(&mailbox.id, current_modseq.into())
|
||||
.await?
|
||||
{
|
||||
// Synchronize messages
|
||||
let mut current_state = mailbox.state.lock();
|
||||
current_modseq = new_state.modseq;
|
||||
|
||||
// Add missing uids
|
||||
let mut deletions = current_state
|
||||
.next_state
|
||||
.take()
|
||||
.map(|state| state.deletions)
|
||||
.unwrap_or_default();
|
||||
let mut id_to_imap = AHashMap::with_capacity(current_state.id_to_imap.len());
|
||||
for (id, imap_id) in std::mem::take(&mut current_state.id_to_imap) {
|
||||
if !new_state.uid_to_id.contains_key(&imap_id.uid) {
|
||||
// Add to deletions
|
||||
deletions.push(imap_id);
|
||||
|
||||
// Invalidate entries
|
||||
current_state.uid_to_id.remove(&imap_id.uid);
|
||||
} else {
|
||||
id_to_imap.insert(id, imap_id);
|
||||
}
|
||||
}
|
||||
current_state.id_to_imap = id_to_imap;
|
||||
|
||||
// Update state
|
||||
current_state.modseq = new_state.modseq;
|
||||
current_state.next_state = Some(Box::new(NextMailboxState {
|
||||
next_state: new_state,
|
||||
deletions,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(current_modseq)
|
||||
}
|
||||
|
||||
pub async fn write_mailbox_changes(
|
||||
&self,
|
||||
mailbox: &SelectedMailbox,
|
||||
use_vanished: bool,
|
||||
) -> trc::Result<u64> {
|
||||
// Resync mailbox
|
||||
let modseq = self.synchronize_messages(mailbox).await?;
|
||||
let mut buf = Vec::new();
|
||||
{
|
||||
let mut current_state = mailbox.state.lock();
|
||||
if let Some(next_state) = current_state.next_state.take() {
|
||||
if !next_state.deletions.is_empty() {
|
||||
let mut ids = next_state
|
||||
.deletions
|
||||
.into_iter()
|
||||
.map(|id| if use_vanished { id.uid } else { id.seqnum })
|
||||
.collect::<Vec<u32>>();
|
||||
ids.sort_unstable();
|
||||
expunge::Response { use_vanished, ids }.serialize_to(&mut buf);
|
||||
}
|
||||
if !buf.is_empty()
|
||||
|| next_state
|
||||
.next_state
|
||||
.uid_max
|
||||
.saturating_sub(current_state.uid_max)
|
||||
> 0
|
||||
{
|
||||
Exists {
|
||||
total_messages: next_state.next_state.total_messages,
|
||||
}
|
||||
.serialize(&mut buf);
|
||||
}
|
||||
*current_state = next_state.next_state;
|
||||
}
|
||||
}
|
||||
if !buf.is_empty() {
|
||||
self.write_bytes(buf).await?;
|
||||
}
|
||||
|
||||
Ok(modseq)
|
||||
}
|
||||
|
||||
pub async fn get_uid_next(&self, mailbox: &MailboxId) -> trc::Result<u32> {
|
||||
self.server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_counter(ValueKey {
|
||||
account_id: mailbox.account_id,
|
||||
collection: Collection::Mailbox.into(),
|
||||
document_id: mailbox.mailbox_id,
|
||||
class: ValueClass::Property(MailboxField::UidCounter.into()),
|
||||
})
|
||||
.await
|
||||
.map(|v| (v + 1) as u32)
|
||||
}
|
||||
|
||||
pub fn mailbox_state(&self, mailbox: &MailboxId) -> Option<Mailbox> {
|
||||
self.mailboxes
|
||||
.lock()
|
||||
.iter()
|
||||
.find(|m| m.account_id == mailbox.account_id)
|
||||
.and_then(|m| m.mailbox_state.get(&mailbox.mailbox_id))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedMailbox {
|
||||
pub async fn sequence_to_ids(
|
||||
&self,
|
||||
sequence: &Sequence,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<AHashMap<u32, ImapId>> {
|
||||
if !sequence.is_saved_search() {
|
||||
let mut ids = AHashMap::new();
|
||||
let state = self.state.lock();
|
||||
let (id_to_imap, uid_max, total_messages) =
|
||||
if let Some(next) = state.next_state.as_ref() {
|
||||
(
|
||||
&next.next_state.id_to_imap,
|
||||
next.next_state.uid_max,
|
||||
next.next_state.total_messages,
|
||||
)
|
||||
} else {
|
||||
(&state.id_to_imap, state.uid_max, state.total_messages)
|
||||
};
|
||||
|
||||
if is_uid {
|
||||
if !id_to_imap.is_empty() {
|
||||
for (id, imap_id) in id_to_imap {
|
||||
if sequence.contains(imap_id.uid, uid_max) {
|
||||
ids.insert(*id, *imap_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !id_to_imap.is_empty() {
|
||||
for (id, imap_id) in id_to_imap {
|
||||
if sequence.contains(imap_id.seqnum, total_messages as u32) {
|
||||
ids.insert(*id, *imap_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
} else {
|
||||
let saved_ids = self.get_saved_search().await.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("No saved search found.")
|
||||
})?;
|
||||
let mut ids = AHashMap::with_capacity(saved_ids.len());
|
||||
let state = self.state.lock();
|
||||
|
||||
for imap_id in saved_ids.iter() {
|
||||
if let Some(id) = state.uid_to_id.get(&imap_id.uid) {
|
||||
ids.insert(*id, *imap_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uids_in_range(&self, min: Option<u32>, max: Option<u32>) -> RoaringBitmap {
|
||||
let state = self.state.lock();
|
||||
let id_to_imap = state
|
||||
.next_state
|
||||
.as_ref()
|
||||
.map_or(&state.id_to_imap, |next| &next.next_state.id_to_imap);
|
||||
|
||||
RoaringBitmap::from_iter(id_to_imap.iter().filter_map(|(id, imap_id)| {
|
||||
(min.is_none_or(|min| imap_id.uid >= min) && max.is_none_or(|max| imap_id.uid <= max))
|
||||
.then_some(*id)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn seqnum_to_uid(&self, seqnum: u32) -> Option<u32> {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.next_state
|
||||
.as_ref()
|
||||
.map_or(&state.id_to_imap, |next| &next.next_state.id_to_imap)
|
||||
.values()
|
||||
.find(|imap_id| imap_id.seqnum == seqnum)
|
||||
.map(|imap_id| imap_id.uid)
|
||||
}
|
||||
|
||||
pub fn uids_descending(&self) -> Vec<u32> {
|
||||
let mut uids = {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.next_state
|
||||
.as_ref()
|
||||
.map_or(&state.uid_to_id, |next| &next.next_state.uid_to_id)
|
||||
.keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
uids.sort_unstable_by(|a, b| b.cmp(a));
|
||||
uids
|
||||
}
|
||||
|
||||
pub async fn sequence_expand_missing(&self, sequence: &Sequence, is_uid: bool) -> Vec<u32> {
|
||||
let mut deleted_ids = Vec::new();
|
||||
if !sequence.is_saved_search() {
|
||||
let state = self.state.lock();
|
||||
if is_uid {
|
||||
for uid in sequence.expand(state.uid_max) {
|
||||
if !state.uid_to_id.contains_key(&uid) {
|
||||
deleted_ids.push(uid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for seqnum in sequence.expand(state.total_messages as u32) {
|
||||
if seqnum > state.total_messages as u32 {
|
||||
deleted_ids.push(seqnum);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(saved_ids) = self.get_saved_search().await {
|
||||
let state = self.state.lock();
|
||||
for id in saved_ids.iter() {
|
||||
if !state.uid_to_id.contains_key(&id.uid) {
|
||||
deleted_ids.push(if is_uid { id.uid } else { id.seqnum });
|
||||
}
|
||||
}
|
||||
}
|
||||
deleted_ids.sort_unstable();
|
||||
deleted_ids
|
||||
}
|
||||
|
||||
pub fn append_messages(&self, ids: Vec<ImapUidToId>, modseq: Option<u64>) {
|
||||
let mut mailbox = self.state.lock();
|
||||
if modseq.unwrap_or(0) > mailbox.modseq {
|
||||
let mut uid_max = 0;
|
||||
for id in ids {
|
||||
mailbox.total_messages += 1;
|
||||
let seqnum = mailbox.total_messages as u32;
|
||||
mailbox.uid_to_id.insert(id.uid, id.uid);
|
||||
mailbox.id_to_imap.insert(
|
||||
id.id,
|
||||
ImapId {
|
||||
uid: id.uid,
|
||||
seqnum,
|
||||
},
|
||||
);
|
||||
uid_max = id.uid;
|
||||
}
|
||||
mailbox.uid_max = uid_max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
Inner, Server,
|
||||
auth::AccessToken,
|
||||
network::{ServerInstance, SessionStream, limiter::InFlight},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command,
|
||||
protocol::{ProtocolVersion, list::Attribute},
|
||||
receiver::Receiver,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
net::IpAddr,
|
||||
sync::{Arc, atomic::AtomicU32},
|
||||
};
|
||||
use tokio::{
|
||||
io::{ReadHalf, WriteHalf},
|
||||
sync::watch,
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
pub mod client;
|
||||
pub mod mailbox;
|
||||
pub mod message;
|
||||
pub mod session;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImapSessionManager {
|
||||
pub inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
impl ImapSessionManager {
|
||||
pub fn new(inner: Arc<Inner>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Session<T: SessionStream> {
|
||||
pub server: Server,
|
||||
pub instance: Arc<ServerInstance>,
|
||||
pub receiver: Receiver<Command>,
|
||||
pub version: ProtocolVersion,
|
||||
pub state: State<T>,
|
||||
pub is_tls: bool,
|
||||
pub is_condstore: bool,
|
||||
pub is_qresync: bool,
|
||||
pub is_utf8: bool,
|
||||
pub is_objectid: bool,
|
||||
pub is_uidonly: bool,
|
||||
pub stream_rx: ReadHalf<T>,
|
||||
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
|
||||
pub in_flight: InFlight,
|
||||
pub remote_addr: IpAddr,
|
||||
pub session_id: u64,
|
||||
}
|
||||
|
||||
pub struct SessionData<T: SessionStream> {
|
||||
pub account_id: u32,
|
||||
pub access_token: AccessToken,
|
||||
pub server: Server,
|
||||
pub session_id: u64,
|
||||
pub mailboxes: parking_lot::Mutex<Vec<Account>>,
|
||||
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
|
||||
pub state: AtomicU32,
|
||||
pub remote_addr: IpAddr,
|
||||
pub in_flight: Option<InFlight>,
|
||||
}
|
||||
|
||||
pub struct SelectedMailbox {
|
||||
pub id: MailboxId,
|
||||
pub state: parking_lot::Mutex<MailboxState>,
|
||||
pub saved_search: parking_lot::Mutex<SavedSearch>,
|
||||
pub is_select: bool,
|
||||
pub is_condstore: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub struct MailboxId {
|
||||
pub account_id: u32,
|
||||
pub mailbox_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Account {
|
||||
pub account_id: u32,
|
||||
pub prefix: Option<String>,
|
||||
pub mailbox_names: BTreeMap<String, u32>,
|
||||
pub mailbox_state: AHashMap<u32, Mailbox>,
|
||||
pub last_change_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Mailbox {
|
||||
pub has_children: bool,
|
||||
pub is_subscribed: bool,
|
||||
pub special_use: Option<Attribute>,
|
||||
pub total_messages: u64,
|
||||
pub total_unseen: u64,
|
||||
pub total_deleted: u64,
|
||||
pub total_deleted_storage: Option<u64>,
|
||||
pub uid_validity: u64,
|
||||
pub uid_next: u64,
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MailboxState {
|
||||
pub uid_max: u32,
|
||||
pub id_to_imap: AHashMap<u32, ImapId>,
|
||||
pub uid_to_id: AHashMap<u32, u32>,
|
||||
pub total_messages: usize,
|
||||
pub modseq: u64,
|
||||
pub next_state: Option<Box<NextMailboxState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NextMailboxState {
|
||||
pub next_state: MailboxState,
|
||||
pub deletions: Vec<ImapId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ImapId {
|
||||
pub uid: u32,
|
||||
pub seqnum: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MailboxSync {
|
||||
pub added: Vec<String>,
|
||||
pub changed: Vec<String>,
|
||||
pub deleted: Vec<String>,
|
||||
}
|
||||
|
||||
pub enum SavedSearch {
|
||||
InFlight {
|
||||
rx: watch::Receiver<Arc<Vec<ImapId>>>,
|
||||
},
|
||||
Results {
|
||||
items: Arc<Vec<ImapId>>,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ImapUidToId {
|
||||
pub uid: u32,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
pub enum State<T: SessionStream> {
|
||||
NotAuthenticated {
|
||||
auth_failures: u32,
|
||||
},
|
||||
Authenticated {
|
||||
data: Arc<SessionData<T>>,
|
||||
},
|
||||
Selected {
|
||||
data: Arc<SessionData<T>>,
|
||||
mailbox: Arc<SelectedMailbox>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: SessionStream> State<T> {
|
||||
pub fn try_replace_stream_tx<U: SessionStream>(
|
||||
self,
|
||||
new_stream: Arc<tokio::sync::Mutex<WriteHalf<U>>>,
|
||||
) -> Option<State<U>> {
|
||||
match self {
|
||||
State::NotAuthenticated { auth_failures } => {
|
||||
State::NotAuthenticated { auth_failures }.into()
|
||||
}
|
||||
State::Authenticated { data } => {
|
||||
Arc::try_unwrap(data).ok().map(|data| State::Authenticated {
|
||||
data: Arc::new(data.replace_stream_tx(new_stream)),
|
||||
})
|
||||
}
|
||||
State::Selected { data, mailbox } => {
|
||||
Arc::try_unwrap(data).ok().map(|data| State::Selected {
|
||||
data: Arc::new(data.replace_stream_tx(new_stream)),
|
||||
mailbox,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn refresh_access_token(&self) -> trc::Result<AccessToken> {
|
||||
self.server
|
||||
.access_token(self.account_id)
|
||||
.await
|
||||
.and_then(|inner| {
|
||||
AccessToken::renew(inner, self.access_token.credential_id(), self.remote_addr)
|
||||
})
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub fn replace_stream_tx<U: SessionStream>(
|
||||
self,
|
||||
new_stream: Arc<tokio::sync::Mutex<WriteHalf<U>>>,
|
||||
) -> SessionData<U> {
|
||||
SessionData {
|
||||
account_id: self.account_id,
|
||||
server: self.server,
|
||||
session_id: self.session_id,
|
||||
mailboxes: self.mailboxes,
|
||||
stream_tx: new_stream,
|
||||
state: self.state,
|
||||
in_flight: self.in_flight,
|
||||
access_token: self.access_token,
|
||||
remote_addr: self.remote_addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxState {
|
||||
pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> {
|
||||
if let Some(imap_id) = self.id_to_imap.get(&document_id) {
|
||||
Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id))
|
||||
} else if is_uid {
|
||||
self.next_state.as_ref().and_then(|s| {
|
||||
s.next_state
|
||||
.id_to_imap
|
||||
.get(&document_id)
|
||||
.map(|imap_id| (imap_id.uid, *imap_id))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapSessionManager, Session, State};
|
||||
use crate::{
|
||||
GREETING_WITH_TLS, GREETING_WITH_TLS_LOGIN_DISABLED, GREETING_WITHOUT_TLS,
|
||||
GREETING_WITHOUT_TLS_LOGIN_DISABLED,
|
||||
};
|
||||
use common::{
|
||||
BuildServer,
|
||||
network::{SessionData, SessionManager, SessionResult, SessionStream, stream::NullIo},
|
||||
};
|
||||
use imap_proto::{
|
||||
protocol::{ProtocolVersion, SerializeResponse},
|
||||
receiver::Receiver,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio_rustls::server::TlsStream;
|
||||
|
||||
impl SessionManager for ImapSessionManager {
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
fn handle<T: SessionStream>(
|
||||
self,
|
||||
session: SessionData<T>,
|
||||
) -> impl std::future::Future<Output = ()> + Send {
|
||||
async move {
|
||||
if let Ok(mut session) = Session::new(session, self).await
|
||||
&& 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_rx.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"* BYE 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 new(
|
||||
mut session: SessionData<T>,
|
||||
manager: ImapSessionManager,
|
||||
) -> Result<Session<T>, ()> {
|
||||
// Write greeting
|
||||
let is_tls = session.stream.is_tls();
|
||||
let server = manager.inner.build_server();
|
||||
let offer_tls = !is_tls && session.instance.acceptor.is_tls();
|
||||
let allow_auth = is_tls || server.core.imap.allow_plain_auth;
|
||||
let greeting = match (offer_tls, allow_auth) {
|
||||
(true, true) => &GREETING_WITH_TLS,
|
||||
(true, false) => &GREETING_WITH_TLS_LOGIN_DISABLED,
|
||||
(false, true) => &GREETING_WITHOUT_TLS,
|
||||
(false, false) => &GREETING_WITHOUT_TLS_LOGIN_DISABLED,
|
||||
};
|
||||
|
||||
if let Err(err) = session.stream.write_all(greeting).await {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::WriteError),
|
||||
Reason = err.to_string(),
|
||||
SpanId = session.session_id,
|
||||
Details = "Failed to write to stream"
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
let _ = session.stream.flush().await;
|
||||
|
||||
// Split stream into read and write halves
|
||||
let (stream_rx, stream_tx) = tokio::io::split(session.stream);
|
||||
|
||||
Ok(Session {
|
||||
receiver: Receiver::with_max_request_size(server.core.imap.max_request_size),
|
||||
version: ProtocolVersion::Rev1,
|
||||
state: State::NotAuthenticated { auth_failures: 0 },
|
||||
is_tls,
|
||||
is_condstore: false,
|
||||
is_qresync: false,
|
||||
is_utf8: false,
|
||||
is_objectid: false,
|
||||
is_uidonly: false,
|
||||
server,
|
||||
instance: session.instance,
|
||||
session_id: session.session_id,
|
||||
in_flight: session.in_flight,
|
||||
remote_addr: session.remote_ip,
|
||||
stream_rx,
|
||||
stream_tx: Arc::new(tokio::sync::Mutex::new(stream_tx)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn into_tls(self) -> Result<Session<TlsStream<T>>, ()> {
|
||||
// Drop references to write half from state
|
||||
let state = if let Some(state) =
|
||||
self.state
|
||||
.try_replace_stream_tx(Arc::new(tokio::sync::Mutex::new(
|
||||
tokio::io::split(NullIo::default()).1,
|
||||
))) {
|
||||
state
|
||||
} else {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::SplitError),
|
||||
SpanId = self.session_id,
|
||||
Details = "Failed to obtain write half state"
|
||||
);
|
||||
return Err(());
|
||||
};
|
||||
|
||||
// Take ownership of WriteHalf and unsplit it from ReadHalf
|
||||
let stream = if let Ok(stream_tx) =
|
||||
Arc::try_unwrap(self.stream_tx).map(|mutex| mutex.into_inner())
|
||||
{
|
||||
self.stream_rx.unsplit(stream_tx)
|
||||
} else {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::SplitError),
|
||||
SpanId = self.session_id,
|
||||
Details = "Failed to take ownership of write half"
|
||||
);
|
||||
|
||||
return Err(());
|
||||
};
|
||||
|
||||
// Upgrade to TLS
|
||||
let (stream_rx, stream_tx) =
|
||||
tokio::io::split(self.instance.tls_accept(stream, self.session_id).await?);
|
||||
let stream_tx = Arc::new(tokio::sync::Mutex::new(stream_tx));
|
||||
let receiver = Receiver::with_max_request_size(self.server.core.imap.max_request_size);
|
||||
|
||||
Ok(Session {
|
||||
server: self.server,
|
||||
instance: self.instance,
|
||||
receiver,
|
||||
version: self.version,
|
||||
state: state.try_replace_stream_tx(stream_tx.clone()).unwrap(),
|
||||
is_tls: true,
|
||||
is_condstore: self.is_condstore,
|
||||
is_qresync: self.is_qresync,
|
||||
is_utf8: self.is_utf8,
|
||||
is_objectid: self.is_objectid,
|
||||
is_uidonly: self.is_uidonly,
|
||||
session_id: self.session_id,
|
||||
in_flight: self.in_flight,
|
||||
remote_addr: self.remote_addr,
|
||||
stream_rx,
|
||||
stream_tx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub fn activate_objectid(&mut self) -> Option<&'static [u8]> {
|
||||
if self.is_objectid {
|
||||
None
|
||||
} else {
|
||||
self.is_objectid = true;
|
||||
Some(b"* ENABLED OBJECTID+\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_bytes(&self, bytes: impl AsRef<[u8]>) -> trc::Result<()> {
|
||||
let bytes = bytes.as_ref();
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::RawOutput),
|
||||
SpanId = self.session_id,
|
||||
Size = bytes.len(),
|
||||
Contents = trc::Value::from_maybe_string(bytes),
|
||||
);
|
||||
|
||||
let mut stream = self.stream_tx.lock().await;
|
||||
if let Err(err) = stream.write_all(bytes).await {
|
||||
Err(trc::NetworkEvent::WriteError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.details("Failed to write to stream"))
|
||||
} else {
|
||||
let _ = stream.flush().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_error(&self, err: trc::Error) -> bool {
|
||||
if err.should_write_err() {
|
||||
let disconnect = err.must_disconnect();
|
||||
let bytes = err.serialize();
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
|
||||
if let Err(err) = self.write_bytes(bytes).await {
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
false
|
||||
} else {
|
||||
!disconnect
|
||||
}
|
||||
} else {
|
||||
trc::error!(err);
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> super::SessionData<T> {
|
||||
pub async fn write_bytes(&self, bytes: impl AsRef<[u8]>) -> trc::Result<()> {
|
||||
let bytes = bytes.as_ref();
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::RawOutput),
|
||||
SpanId = self.session_id,
|
||||
Size = bytes.len(),
|
||||
Contents = trc::Value::from_maybe_string(bytes),
|
||||
);
|
||||
|
||||
let mut stream = self.stream_tx.lock().await;
|
||||
if let Err(err) = stream.write_all(bytes.as_ref()).await {
|
||||
Err(trc::NetworkEvent::WriteError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.details("Failed to write to stream"))
|
||||
} else {
|
||||
let _ = stream.flush().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_error(&self, err: trc::Error) -> trc::Result<()> {
|
||||
if err.should_write_err() {
|
||||
let bytes = err.serialize();
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
self.write_bytes(bytes).await
|
||||
} else {
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user