Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+288
View File
@@ -0,0 +1,288 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{ImapContext, ToModSeq};
use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
use ahash::AHashMap;
use common::{network::SessionStream, storage::index::ObjectIndexBuilder};
use email::{
cache::{MessageCacheFetch, email::MessageCacheAccess},
message::{delete::EmailDeletion, metadata::MessageData},
};
use imap_proto::{
Command, ResponseCode, ResponseType, StatusResponse,
parser::parse_sequence_set,
receiver::{Request, Token},
};
use registry::schema::{
enums::{IndexDocumentType, Permission},
structs::{Task, TaskIndexDocument, TaskStatus},
};
use std::{sync::Arc, time::Instant};
use store::{roaring::RoaringBitmap, write::BatchBuilder};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, VanishedCollection},
keyword::Keyword,
};
impl<T: SessionStream> Session<T> {
pub async fn handle_expunge(
&mut self,
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapExpunge)?;
let op_start = Instant::now();
let (data, mailbox) = self.state.select_data();
// Validate ACL
if !data
.check_mailbox_acl(
mailbox.id.account_id,
mailbox.id.mailbox_id,
Acl::RemoveItems,
)
.await
.imap_ctx(&request.tag, trc::location!())?
{
return Err(trc::ImapEvent::Error
.into_err()
.details(concat!(
"You do not have the required permissions ",
"to remove messages from this mailbox."
))
.code(ResponseCode::NoPerm)
.id(request.tag));
}
// Parse sequence to operate on
let sequence = match request.tokens.into_iter().next() {
Some(Token::Argument(value)) if is_uid => {
let sequence = parse_sequence_set(&value).map_err(|err| {
trc::ImapEvent::Error
.into_err()
.details(err)
.ctx(trc::Key::Type, ResponseType::Bad)
.id(request.tag.clone())
})?;
Some(
mailbox
.sequence_to_ids(&sequence, true)
.await
.map_err(|err| err.id(request.tag.clone()))?,
)
}
_ => None,
};
// RFC 9738 limits UID EXPUNGE but never a plain EXPUNGE
let message_limit = if is_uid {
self.server.core.imap.max_messages_per_command
} else {
u32::MAX
};
// Expunge
let limited_uid = data
.expunge(mailbox.clone(), sequence, message_limit, op_start)
.await
.imap_ctx(&request.tag, trc::location!())?;
// Clear saved searches
*mailbox.saved_search.lock() = SavedSearch::None;
// Synchronize messages
let modseq = data
.write_mailbox_changes(&mailbox, self.is_qresync || self.is_uidonly)
.await
.imap_ctx(&request.tag, trc::location!())?;
let mut response =
StatusResponse::completed(Command::Expunge(is_uid)).with_tag(request.tag);
let mut untagged = Vec::new();
if let Some(uid) = limited_uid {
let code = ResponseCode::MessageLimit {
limit: message_limit,
uid: uid.into(),
};
if self.is_condstore {
untagged = StatusResponse::ok("Some messages were not expunged.")
.with_code(code)
.into_bytes();
} else {
response = response.with_code(code);
}
}
if self.is_condstore {
response = response.with_code(ResponseCode::HighestModseq {
modseq: modseq.to_modseq(),
});
}
self.write_bytes(response.serialize(untagged)).await
}
}
impl<T: SessionStream> SessionData<T> {
pub async fn expunge(
&self,
mailbox: Arc<SelectedMailbox>,
sequence: Option<AHashMap<u32, ImapId>>,
message_limit: u32,
op_start: Instant,
) -> trc::Result<Option<u32>> {
// Obtain message ids
let account_id = mailbox.id.account_id;
let mut deleted_ids = RoaringBitmap::from_iter(
self.server
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?
.in_mailbox_with_keyword(mailbox.id.mailbox_id, &Keyword::Deleted)
.map(|m| m.document_id),
);
// Filter by sequence
if let Some(sequence) = &sequence {
deleted_ids &= RoaringBitmap::from_iter(sequence.keys());
}
// RFC 9738 requires the highest UIDs to be processed first when truncating.
// Only messages the session has a UID for can be ordered, so the count that
// decides whether to truncate has to come from that same set.
let mut limited_uid = None;
if deleted_ids.len() > message_limit as u64 {
let mut uids = {
let state = mailbox.state.lock();
deleted_ids
.iter()
.filter_map(|id| state.id_to_imap.get(&id).map(|imap_id| (imap_id.uid, id)))
.collect::<Vec<_>>()
};
if uids.len() > message_limit as usize {
let cutoff = uids.len() - message_limit as usize;
let (below, lowest, _) = uids.select_nth_unstable(cutoff);
limited_uid = Some(lowest.0);
for (_, id) in below {
deleted_ids.remove(*id);
}
}
}
// Delete ids
let mut batch = BatchBuilder::new();
let (fully_deleted, thread_ids) = self
.email_untag_or_delete(account_id, mailbox.id.mailbox_id, &deleted_ids, &mut batch)
.await
.caused_by(trc::location!())?;
self.server
.log_emptied_threads(account_id, &mut batch, thread_ids, &fully_deleted)
.await
.caused_by(trc::location!())?;
trc::event!(
Imap(trc::ImapEvent::Expunge),
SpanId = self.session_id,
AccountId = account_id,
MailboxId = mailbox.id.mailbox_id,
DocumentId = deleted_ids.iter().map(trc::Value::from).collect::<Vec<_>>(),
Elapsed = op_start.elapsed()
);
// Write changes on source account
if !batch.is_empty() {
self.server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
self.server.notify_task_queue();
}
Ok(limited_uid)
}
pub async fn email_untag_or_delete(
&self,
account_id: u32,
mailbox_id: u32,
deleted_ids: &RoaringBitmap,
batch: &mut BatchBuilder,
) -> trc::Result<(RoaringBitmap, RoaringBitmap)> {
batch
.with_account_id(account_id)
.with_collection(Collection::Email);
let mut fully_deleted = RoaringBitmap::new();
let mut thread_ids = RoaringBitmap::new();
self.server
.archives(
account_id,
Collection::Email,
deleted_ids,
|document_id, data_| {
let metadata = data_
.to_unarchived::<MessageData>()
.caused_by(trc::location!())?;
if let Some(message_uid) = metadata.inner.message_uid(mailbox_id) {
// Add vanished items
batch.with_document(document_id);
batch.log_vanished_item(
VanishedCollection::Email,
(mailbox_id, message_uid),
);
if metadata.inner.mailboxes.len() == 1 {
// Delete message
fully_deleted.insert(document_id);
thread_ids.insert(metadata.inner.thread_id.to_native());
batch
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(self.access_token.account_tenant_ids())
.with_current(metadata),
)
.caused_by(trc::location!())?
.schedule_task(Task::UnindexDocument(TaskIndexDocument {
account_id: account_id.into(),
document_id: document_id.into(),
document_type: IndexDocumentType::Email,
status: TaskStatus::now(),
}))
.commit_point();
} else {
// Untag message from this mailbox and remove Deleted flag
let mut new_metadata = metadata.inner.to_builder();
new_metadata.remove_mailbox(mailbox_id);
new_metadata.remove_keyword(&Keyword::Deleted);
// Write changes
batch
.custom(
ObjectIndexBuilder::new()
.with_current(metadata)
.with_changes(new_metadata.seal()),
)
.caused_by(trc::location!())?
.commit_point();
}
}
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
Ok((fully_deleted, thread_ids))
}
}