Incremental caching - tests passing

This commit is contained in:
mdecimus 2025-04-10 20:42:51 +02:00
parent b396341240
commit 15651c26a1
28 changed files with 522 additions and 457 deletions

View file

@ -34,7 +34,7 @@ use config::{
};
use ipc::{HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent};
use jmap_proto::types::keyword::Keyword;
use jmap_proto::types::{keyword::Keyword, value::AclGrant};
use listener::{asn::AsnGeoLookupData, blocked::Security, tls::AcmeProviders};
use mail_auth::{MX, Txt};
@ -191,9 +191,11 @@ pub struct MailboxCache {
pub name: CompactString,
pub path: CompactString,
pub role: SpecialUse,
pub parent_id: Option<u32>,
pub parent_id: u32,
pub sort_order: u32,
pub subscribers: TinyVec<[u32; 4]>,
pub uid_validity: u32,
pub acls: TinyVec<[AclGrant; 2]>,
}
#[derive(Debug, Clone, Default)]
@ -560,3 +562,25 @@ impl<T> CacheSwap<T> {
self.0.store(value);
}
}
impl MailboxCache {
pub fn parent_id(&self) -> Option<u32> {
if self.parent_id != u32::MAX {
Some(self.parent_id)
} else {
None
}
}
pub fn sort_order(&self) -> Option<u32> {
if self.sort_order != u32::MAX {
Some(self.sort_order)
} else {
None
}
}
pub fn is_root(&self) -> bool {
self.parent_id == u32::MAX
}
}

View file

@ -13,7 +13,7 @@ use jmap_proto::{
types::{
acl::Acl,
property::Property,
value::{AclGrant, ArchivedAclGrant, MaybePatchValue, Value},
value::{AclGrant, MaybePatchValue, Value},
},
};
use utils::map::bitmap::Bitmap;
@ -80,14 +80,13 @@ impl Server {
pub async fn acl_get(
&self,
value: &[ArchivedAclGrant],
value: &[AclGrant],
access_token: &AccessToken,
account_id: u32,
) -> Value {
if access_token.is_member(account_id)
|| value.iter().any(|item| {
access_token.is_member(item.account_id.into())
&& Bitmap::from(&item.grants).contains(Acl::Administer)
access_token.is_member(item.account_id) && item.grants.contains(Acl::Administer)
})
{
let mut acl_obj = jmap_proto::types::value::Object::with_capacity(value.len() / 2);
@ -96,13 +95,13 @@ impl Server {
.core
.storage
.directory
.query(QueryBy::Id(item.account_id.into()), false)
.query(QueryBy::Id(item.account_id), false)
.await
.unwrap_or_default()
{
acl_obj.append(
Property::_T(principal.take_str(PrincipalField::Name).unwrap_or_default()),
Bitmap::from(&item.grants)
item.grants
.map(|acl_item| Value::Text(acl_item.to_string()))
.collect::<Vec<_>>(),
);

View file

@ -21,9 +21,15 @@ pub trait EffectiveAcl {
}
impl EffectiveAcl for Vec<AclGrant> {
fn effective_acl(&self, access_token: &AccessToken) -> Bitmap<Acl> {
self.as_slice().effective_acl(access_token)
}
}
impl EffectiveAcl for &[AclGrant] {
fn effective_acl(&self, access_token: &AccessToken) -> Bitmap<Acl> {
let mut acl = Bitmap::<Acl>::new();
for item in self {
for item in self.iter() {
if access_token.is_member(item.account_id) {
acl.union(&item.grants);
}

View file

@ -7,18 +7,20 @@
use std::sync::Arc;
use common::{
CacheSwap, MailboxCache, MessageStoreCache, Server, config::jmap::settings::SpecialUse,
CacheSwap, MailboxCache, MessageStoreCache, Server, auth::AccessToken,
config::jmap::settings::SpecialUse, sharing::EffectiveAcl,
};
use compact_str::CompactString;
use jmap_proto::types::collection::Collection;
use jmap_proto::types::{acl::Acl, collection::Collection, value::AclGrant};
use std::future::Future;
use store::{
ahash::AHashMap,
query::log::{Change, Query},
roaring::RoaringBitmap,
};
use tokio::sync::Semaphore;
use trc::AddContext;
use utils::topological::TopologicalSort;
use utils::{map::bitmap::Bitmap, topological::TopologicalSort};
use super::{ArchivedMailbox, Mailbox, manage::MailboxFnc};
@ -194,12 +196,25 @@ fn insert_item(
path: "".into(),
role: (&mailbox.role).into(),
parent_id: if parent_id > 0 {
Some(parent_id - 1)
parent_id - 1
} else {
None
u32::MAX
},
sort_order: mailbox
.sort_order
.as_ref()
.map(|s| s.to_native())
.unwrap_or(u32::MAX),
subscribers: mailbox.subscribers.iter().map(|s| s.to_native()).collect(),
uid_validity: mailbox.uid_validity.to_native(),
acls: mailbox
.acls
.iter()
.map(|acl| AclGrant {
account_id: acl.account_id.to_native(),
grants: Bitmap::from(&acl.grants),
})
.collect(),
};
cache.items.insert(document_id, item);
@ -211,13 +226,16 @@ fn build_tree(cache: &mut MessageStoreCache<MailboxCache>) {
for (idx, (&document_id, mailbox)) in cache.items.iter_mut().enumerate() {
topological_sort.insert(
mailbox.parent_id.map(|id| id + 1).unwrap_or(0),
if mailbox.parent_id == u32::MAX {
0
} else {
mailbox.parent_id + 1
},
document_id + 1,
);
mailbox.path = if matches!(mailbox.role, SpecialUse::Inbox) {
"INBOX".into()
} else if mailbox.parent_id.is_none() && mailbox.name.as_str().eq_ignore_ascii_case("inbox")
{
} else if mailbox.is_root() && mailbox.name.as_str().eq_ignore_ascii_case("inbox") {
format!("INBOX {}", idx + 1).into()
} else {
mailbox.name.clone()
@ -235,7 +253,11 @@ fn build_tree(cache: &mut MessageStoreCache<MailboxCache>) {
if let Some((path, parent_path)) = cache
.items
.get(&folder_id)
.and_then(|folder| folder.parent_id.map(|parent_id| (&folder.path, parent_id)))
.and_then(|folder| {
folder
.parent_id()
.map(|parent_id| (&folder.path, parent_id))
})
.and_then(|(path, parent_id)| {
cache
.items
@ -258,6 +280,11 @@ pub trait MailboxCacheAccess {
fn by_name(&self, name: &str) -> Option<(&u32, &MailboxCache)>;
fn by_path(&self, name: &str) -> Option<(&u32, &MailboxCache)>;
fn by_role(&self, role: &SpecialUse) -> Option<(&u32, &MailboxCache)>;
fn shared_mailboxes(
&self,
access_token: &AccessToken,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> RoaringBitmap;
}
impl MailboxCacheAccess for MessageStoreCache<MailboxCache> {
@ -276,4 +303,24 @@ impl MailboxCacheAccess for MessageStoreCache<MailboxCache> {
fn by_role(&self, role: &SpecialUse) -> Option<(&u32, &MailboxCache)> {
self.items.iter().find(|(_, m)| &m.role == role)
}
fn shared_mailboxes(
&self,
access_token: &AccessToken,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> RoaringBitmap {
let check_acls = check_acls.into();
RoaringBitmap::from_iter(
self.items
.iter()
.filter(|(_, m)| {
m.acls
.as_slice()
.effective_acl(access_token)
.contains_all(check_acls)
})
.map(|(id, _)| *id),
)
}
}

View file

@ -12,7 +12,7 @@ use jmap_proto::{
error::set::{SetError, SetErrorType},
types::{acl::Acl, collection::Collection, property::Property},
};
use store::{SerializeInfallible, query::Filter, roaring::RoaringBitmap, write::BatchBuilder};
use store::{roaring::RoaringBitmap, write::BatchBuilder};
use trc::AddContext;
use crate::message::{
@ -21,7 +21,7 @@ use crate::message::{
metadata::MessageData,
};
use super::*;
use super::{cache::MessageMailboxCache, *};
pub trait MailboxDestroy: Sync + Send {
fn mailbox_destroy(
@ -61,19 +61,12 @@ impl MailboxDestroy for Server {
}
// Verify that this mailbox does not have sub-mailboxes
if !self
.store()
.filter(
account_id,
Collection::Mailbox,
vec![Filter::eq(
Property::ParentId,
(document_id + 1).serialize(),
)],
)
if self
.get_cached_mailboxes(account_id)
.await?
.results
.is_empty()
.items
.iter()
.any(|(_, m)| m.parent_id == document_id)
{
return Ok(Err(SetError::new(SetErrorType::MailboxHasChild)
.with_description("Mailbox has at least one children.")));
@ -84,18 +77,19 @@ impl MailboxDestroy for Server {
batch.with_account_id(account_id);
if remove_emails {
// If the message is in multiple mailboxes, untag it from the current mailbox,
// otherwise delete it.
let message_ids = RoaringBitmap::from_iter(
self.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?
.in_mailbox(document_id)
.map(|(id, _)| id),
);
let message_ids = RoaringBitmap::from_iter(
self.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?
.in_mailbox(document_id)
.map(|(id, _)| id),
);
if !message_ids.is_empty() {
if remove_emails {
// If the message is in multiple mailboxes, untag it from the current mailbox,
// otherwise delete it.
if !message_ids.is_empty() {
let mut destroy_ids = RoaringBitmap::new();
self.get_archives(
@ -152,10 +146,10 @@ impl MailboxDestroy for Server {
self.emails_tombstone(account_id, &mut batch, destroy_ids)
.await?;
}
} else {
return Ok(Err(SetError::new(SetErrorType::MailboxHasEmail)
.with_description("Mailbox is not empty.")));
}
} else {
return Ok(Err(SetError::new(SetErrorType::MailboxHasEmail)
.with_description("Mailbox is not empty.")));
}
// Obtain mailbox

View file

@ -4,48 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
config::jmap::settings::{ArchivedSpecialUse, SpecialUse},
storage::{
folder::FolderHierarchy,
index::{IndexValue, IndexableAndSerializableObject, IndexableObject},
},
use common::storage::{
folder::FolderHierarchy,
index::{IndexValue, IndexableAndSerializableObject, IndexableObject},
};
use jmap_proto::types::{property::Property, value::AclGrant};
use jmap_proto::types::value::AclGrant;
use super::{ArchivedMailbox, Mailbox};
impl IndexableObject for Mailbox {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Index {
field: Property::Name.into(),
value: self.name.to_lowercase().into(),
},
IndexValue::Index {
field: Property::Role.into(),
value: self.role.as_str().into(),
},
IndexValue::Tag {
field: Property::Role.into(),
value: if !matches!(self.role, SpecialUse::None) {
vec![().into()]
} else {
vec![]
},
},
IndexValue::Index {
field: Property::ParentId.into(),
value: self.parent_id.into(),
},
IndexValue::Index {
field: Property::SortOrder.into(),
value: self.sort_order.into(),
},
IndexValue::IndexList {
field: Property::IsSubscribed.into(),
value: self.subscribers.iter().map(Into::into).collect::<Vec<_>>(),
},
IndexValue::LogChild { prefix: None },
IndexValue::Acl {
value: (&self.acls).into(),
@ -58,34 +27,6 @@ impl IndexableObject for Mailbox {
impl IndexableObject for &ArchivedMailbox {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Index {
field: Property::Name.into(),
value: self.name.to_lowercase().into(),
},
IndexValue::Index {
field: Property::Role.into(),
value: self.role.as_str().into(),
},
IndexValue::Tag {
field: Property::Role.into(),
value: if !matches!(self.role, ArchivedSpecialUse::None) {
vec![().into()]
} else {
vec![]
},
},
IndexValue::Index {
field: Property::ParentId.into(),
value: self.parent_id.into(),
},
IndexValue::Index {
field: Property::SortOrder.into(),
value: self.sort_order.into(),
},
IndexValue::IndexList {
field: Property::IsSubscribed.into(),
value: self.subscribers.iter().map(Into::into).collect::<Vec<_>>(),
},
IndexValue::LogChild { prefix: None },
IndexValue::Acl {
value: self

View file

@ -104,7 +104,7 @@ impl MailboxFnc for Server {
if let Some((document_id, _)) = folders
.items
.iter()
.find(|(_, item)| item.path == found_path)
.find(|(_, item)| item.path.to_lowercase() == found_path)
{
next_parent_id = *document_id + 1;
} else {

View file

@ -7,7 +7,8 @@
use std::sync::Arc;
use common::{
CacheSwap, MessageItemCache, MessageStoreCache, MessageUidCache, Server, auth::AccessToken,
CacheSwap, MailboxCache, MessageItemCache, MessageStoreCache, MessageUidCache, Server,
auth::AccessToken, sharing::EffectiveAcl,
};
use jmap_proto::types::{acl::Acl, collection::Collection, keyword::Keyword};
use std::future::Future;
@ -27,20 +28,6 @@ pub trait MessageCache: Sync + Send {
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<Arc<MessageStoreCache<MessageItemCache>>>> + Send;
fn shared_messages(
&self,
access_token: &AccessToken,
to_account_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn owned_or_shared_messages(
&self,
access_token: &AccessToken,
account_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
}
impl MessageCache for Server {
@ -153,50 +140,6 @@ impl MessageCache for Server {
Ok(cache)
}
async fn shared_messages(
&self,
access_token: &AccessToken,
to_account_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> trc::Result<RoaringBitmap> {
let check_acls = check_acls.into();
let shared_containers = self
.shared_containers(access_token, to_account_id, Collection::Mailbox, check_acls)
.await?;
if shared_containers.is_empty() {
return Ok(shared_containers);
}
let mut shared_messages = RoaringBitmap::new();
for document_id in shared_containers {
shared_messages.extend(
self.get_cached_messages(to_account_id)
.await?
.in_mailbox(document_id)
.map(|(id, _)| *id),
);
}
Ok(shared_messages)
}
async fn owned_or_shared_messages(
&self,
access_token: &AccessToken,
account_id: u32,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> trc::Result<RoaringBitmap> {
let mut document_ids = self
.get_document_ids(account_id, Collection::Email)
.await?
.unwrap_or_default();
if !document_ids.is_empty() && !access_token.is_member(account_id) {
document_ids &= self
.shared_messages(access_token, account_id, check_acls)
.await?;
}
Ok(document_ids)
}
}
async fn full_cache_build(
@ -279,6 +222,15 @@ pub trait MessageCacheAccess {
mailbox_id: u32,
keyword: &Keyword,
) -> impl Iterator<Item = (&u32, &MessageItemCache)>;
fn document_ids(&self) -> RoaringBitmap;
fn shared_messages(
&self,
access_token: &AccessToken,
mailboxes: &MessageStoreCache<MailboxCache>,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> RoaringBitmap;
}
impl MessageCacheAccess for MessageStoreCache<MessageItemCache> {
@ -319,4 +271,29 @@ impl MessageCacheAccess for MessageStoreCache<MessageItemCache> {
m.mailboxes.iter().any(|m| m.mailbox_id == mailbox_id) && !m.keywords.contains(keyword)
})
}
fn shared_messages(
&self,
access_token: &AccessToken,
mailboxes: &MessageStoreCache<MailboxCache>,
check_acls: impl Into<Bitmap<Acl>> + Sync + Send,
) -> RoaringBitmap {
let check_acls = check_acls.into();
let mut shared_messages = RoaringBitmap::new();
for (mailbox_id, mailbox) in &mailboxes.items {
if mailbox
.acls
.as_slice()
.effective_acl(access_token)
.contains_all(check_acls)
{
shared_messages.extend(self.in_mailbox(*mailbox_id).map(|(id, _)| *id));
}
}
shared_messages
}
fn document_ids(&self) -> RoaringBitmap {
RoaringBitmap::from_iter(self.items.keys())
}
}

View file

@ -16,7 +16,7 @@ use store::{
Serialize, SerializeInfallible,
backend::MAX_TOKEN_LENGTH,
fts::{Field, index::FtsDocument},
write::{Archiver, BatchBuilder, BlobOp, DirectoryClass, TagValue},
write::{Archiver, BatchBuilder, BlobOp, DirectoryClass},
};
use trc::AddContext;
use utils::BlobHash;
@ -577,29 +577,6 @@ impl IndexMessage for BatchBuilder {
impl IndexableObject for MessageData {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Tag {
field: Property::MailboxIds.into(),
value: self
.mailboxes
.iter()
.map(|m| TagValue::Id(m.mailbox_id))
.collect(),
},
IndexValue::Tag {
field: Property::Keywords.into(),
value: self
.keywords
.iter()
.map(|k| match k.id() {
Ok(id) => TagValue::Id(id),
Err(string) => TagValue::Text(string.as_bytes().to_vec()),
})
.collect(),
},
IndexValue::Tag {
field: Property::ThreadId.into(),
value: vec![TagValue::Id(self.thread_id)],
},
IndexValue::LogChild {
prefix: self.thread_id.into(),
},
@ -619,29 +596,6 @@ impl IndexableObject for MessageData {
impl IndexableObject for &ArchivedMessageData {
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
[
IndexValue::Tag {
field: Property::MailboxIds.into(),
value: self
.mailboxes
.iter()
.map(|m| TagValue::Id(u32::from(m.mailbox_id)))
.collect(),
},
IndexValue::Tag {
field: Property::Keywords.into(),
value: self
.keywords
.iter()
.map(|k| match k.id() {
Ok(id) => TagValue::Id(id),
Err(string) => TagValue::Text(string.as_bytes().to_vec()),
})
.collect(),
},
IndexValue::Tag {
field: Property::ThreadId.into(),
value: vec![TagValue::Id(u32::from(self.thread_id))],
},
IndexValue::LogChild {
prefix: self.thread_id.to_native().into(),
},

View file

@ -100,7 +100,7 @@ impl SieveScriptIngest for Server {
// Obtain mailboxIds
let account_id = access_token.primary_id;
let mailbox_cache = self
let mut mailbox_cache = self
.get_cached_mailboxes(account_id)
.await
.caused_by(trc::location!())?;
@ -204,7 +204,7 @@ impl SieveScriptIngest for Server {
match mailbox {
Mailbox::Name(name) => {
if !matches!(
mailbox_cache.by_name(&name),
mailbox_cache.by_path(&name),
Some((document_id, _)) if special_use_ids.is_empty() ||
special_use_ids.contains(document_id)
) {
@ -234,7 +234,7 @@ impl SieveScriptIngest for Server {
{
let role = SpecialUse::parse_value(&role);
if role.is_err()
|| mailbox_cache.by_role(&role.unwrap()).is_some()
|| mailbox_cache.by_role(&role.unwrap()).is_none()
{
result = false;
break;
@ -333,12 +333,18 @@ impl SieveScriptIngest for Server {
// Find mailbox by name
if target_id == u32::MAX {
if !create {
if let Some((document_id, _)) = mailbox_cache.by_name(&folder) {
if let Some((document_id, _)) = mailbox_cache.by_path(&folder) {
target_id = *document_id;
}
} else if let Ok(Some(document_id)) =
self.mailbox_create_path(account_id, &folder).await
} else if let Some(document_id) = self
.mailbox_create_path(account_id, &folder)
.await
.caused_by(trc::location!())?
{
mailbox_cache = self
.get_cached_mailboxes(account_id)
.await
.caused_by(trc::location!())?;
target_id = document_id;
}
}

View file

@ -16,7 +16,10 @@ use common::{
use compact_str::CompactString;
use directory::{QueryBy, backend::internal::PrincipalField};
use email::{
mailbox::{INBOX_ID, cache::MessageMailboxCache},
mailbox::{
INBOX_ID,
cache::{MailboxCacheAccess, MessageMailboxCache},
},
message::cache::{MessageCache, MessageCacheAccess},
};
use imap_proto::protocol::list::Attribute;
@ -114,10 +117,8 @@ impl<T: SessionStream> SessionData<T> {
{
None
} else {
self.server
.shared_containers(access_token, account_id, Collection::Mailbox, Acl::Read)
.await
.caused_by(trc::location!())?
cached_mailboxes
.shared_mailboxes(access_token, Acl::Read)
.into()
};
@ -179,11 +180,10 @@ impl<T: SessionStream> SessionData<T> {
account.mailbox_state.insert(
mailbox_id,
Mailbox {
has_children: cached_mailboxes.items.values().any(|child| {
child
.parent_id
.is_some_and(|parent_id| parent_id == mailbox_id)
}),
has_children: cached_mailboxes
.items
.values()
.any(|child| child.parent_id == mailbox_id),
is_subscribed: mailbox.subscribers.contains(&access_token.primary_id()),
special_use: match mailbox.role {
SpecialUse::Trash => Some(Attribute::Trash),

View file

@ -16,13 +16,14 @@ use common::{
};
use compact_str::CompactString;
use directory::Permission;
use email::mailbox::cache::{MailboxCacheAccess, MessageMailboxCache};
use imap_proto::{
Command, ResponseCode, StatusResponse,
protocol::{create::Arguments, list::Attribute},
receiver::Request,
};
use jmap_proto::types::{acl::Acl, collection::Collection, id::Id, property::Property};
use store::{query::Filter, write::BatchBuilder};
use jmap_proto::types::{acl::Acl, collection::Collection, id::Id};
use store::write::BatchBuilder;
use trc::AddContext;
impl<T: SessionStream> Session<T> {
@ -280,23 +281,21 @@ impl<T: SessionStream> SessionData<T> {
parent_mailbox_name,
special_use: if let Some(mailbox_role) = mailbox_role {
// Make sure role is unique
let role_name = attr_to_role(mailbox_role).as_str().unwrap_or_default();
if !self
let special_use = attr_to_role(mailbox_role);
if self
.server
.store()
.filter(
account_id,
Collection::Mailbox,
vec![Filter::eq(Property::Role, role_name.as_bytes().to_vec())],
)
.get_cached_mailboxes(account_id)
.await
.caused_by(trc::location!())?
.results
.is_empty()
.by_role(&special_use)
.is_some()
{
return Err(trc::ImapEvent::Error
.into_err()
.details(format!("A mailbox with role '{role_name}' already exists.",))
.details(format!(
"A mailbox with role '{}' already exists.",
special_use.as_str().unwrap_or_default()
))
.code(ResponseCode::UseAttr));
}
Some(mailbox_role)

View file

@ -51,7 +51,15 @@ pub enum Value {
pub struct Object<T>(pub VecMap<Property, T>);
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq, Serialize,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Default,
)]
#[rkyv(compare(PartialEq), derive(Debug))]
pub struct AclGrant {

View file

@ -7,7 +7,10 @@
use std::ops::Range;
use common::{Server, auth::AccessToken};
use email::message::cache::MessageCache;
use email::{
mailbox::cache::MessageMailboxCache,
message::cache::{MessageCache, MessageCacheAccess},
};
use jmap_proto::types::{acl::Acl, blob::BlobId, collection::Collection};
use std::future::Future;
use store::BlobClass;
@ -60,12 +63,21 @@ impl BlobDownload for Server {
document_id,
} => {
if Collection::from(*collection) == Collection::Email {
match self
.shared_messages(access_token, *account_id, Acl::ReadItems)
if !self
.get_cached_messages(*account_id)
.await
.caused_by(trc::location!())?
.shared_messages(
access_token,
self.get_cached_mailboxes(*account_id)
.await
.caused_by(trc::location!())?
.as_ref(),
Acl::ReadItems,
)
.contains(*document_id)
{
Ok(shared_messages) if shared_messages.contains(*document_id) => (),
_ => return Ok(None),
return Ok(None);
}
} else {
match self
@ -127,8 +139,17 @@ impl BlobDownload for Server {
if Collection::from(*collection) == Collection::Email {
access_token.is_member(*account_id)
|| self
.shared_messages(access_token, *account_id, Acl::ReadItems)
.await?
.get_cached_messages(*account_id)
.await
.caused_by(trc::location!())?
.shared_messages(
access_token,
self.get_cached_mailboxes(*account_id)
.await
.caused_by(trc::location!())?
.as_ref(),
Acl::ReadItems,
)
.contains(*document_id)
} else {
access_token.is_member(*account_id)

View file

@ -7,8 +7,11 @@
use common::{Server, auth::AccessToken};
use email::{
mailbox::cache::MessageMailboxCache,
message::{cache::MessageCache, copy::EmailCopy},
mailbox::cache::{MailboxCacheAccess, MessageMailboxCache},
message::{
cache::{MessageCache, MessageCacheAccess},
copy::EmailCopy,
},
};
use http_proto::HttpSessionData;
use jmap_proto::{
@ -32,6 +35,7 @@ use jmap_proto::{
value::{MaybePatchValue, Value},
},
};
use trc::AddContext;
use crate::changes::state::StateManager;
use std::future::Future;
@ -76,13 +80,28 @@ impl JmapEmailCopy for Server {
state_change: None,
};
let from_message_ids = self
.owned_or_shared_messages(access_token, from_account_id, Acl::ReadItems)
.await?;
let mailbox_ids = self.get_cached_mailboxes(account_id).await?;
let from_cached_messages = self
.get_cached_messages(from_account_id)
.await
.caused_by(trc::location!())?;
let from_message_ids = if access_token.is_member(from_account_id) {
from_cached_messages.document_ids()
} else {
let from_cached_mailboxes = self
.get_cached_mailboxes(from_account_id)
.await
.caused_by(trc::location!())?;
from_cached_messages.shared_messages(
access_token,
&from_cached_mailboxes,
Acl::ReadItems,
)
};
let cached_mailboxes = self.get_cached_mailboxes(account_id).await?;
let can_add_mailbox_ids = if access_token.is_shared(account_id) {
self.shared_containers(access_token, account_id, Collection::Mailbox, Acl::AddItems)
.await?
cached_mailboxes
.shared_mailboxes(access_token, Acl::AddItems)
.into()
} else {
None
@ -189,7 +208,7 @@ impl JmapEmailCopy for Server {
// Verify that the mailboxIds are valid
for mailbox_id in &mailboxes {
if !mailbox_ids.items.contains_key(mailbox_id) {
if !cached_mailboxes.items.contains_key(mailbox_id) {
response.not_created.append(
id,
SetError::invalid_properties()

View file

@ -6,11 +6,14 @@
use common::{Server, auth::AccessToken};
use email::message::{
cache::MessageCache,
metadata::{
ArchivedGetHeader, ArchivedHeaderName, ArchivedMetadataPartType, MessageData,
MessageMetadata,
use email::{
mailbox::cache::MessageMailboxCache,
message::{
cache::{MessageCache, MessageCacheAccess},
metadata::{
ArchivedGetHeader, ArchivedHeaderName, ArchivedMetadataPartType, MessageData,
MessageMetadata,
},
},
};
use jmap_proto::{
@ -102,15 +105,24 @@ impl EmailGet for Server {
let max_body_value_bytes = request.arguments.max_body_value_bytes.unwrap_or(0);
let account_id = request.account_id.document_id();
let message_ids = self
.owned_or_shared_messages(access_token, account_id, Acl::ReadItems)
.await?;
let cached_messages = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
let message_ids = if access_token.is_member(account_id) {
cached_messages.document_ids()
} else {
let cached_mailboxes = self
.get_cached_mailboxes(account_id)
.await
.caused_by(trc::location!())?;
cached_messages.shared_messages(access_token, &cached_mailboxes, Acl::ReadItems)
};
let ids = if let Some(ids) = ids {
ids
} else {
self.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?
cached_messages
.items
.iter()
.take(self.core.jmap.get_max_objects)

View file

@ -6,7 +6,7 @@
use common::{Server, auth::AccessToken};
use email::{
mailbox::cache::MessageMailboxCache,
mailbox::cache::{MailboxCacheAccess, MessageMailboxCache},
message::ingest::{EmailIngest, IngestEmail, IngestSource},
};
use http_proto::HttpSessionData;
@ -51,10 +51,10 @@ impl EmailImport for Server {
.assert_state(account_id, Collection::Email, &request.if_in_state)
.await?;
let valid_mailbox_ids = self.get_cached_mailboxes(account_id).await?;
let cached_mailboxes = self.get_cached_mailboxes(account_id).await?;
let can_add_mailbox_ids = if access_token.is_shared(account_id) {
self.shared_containers(access_token, account_id, Collection::Mailbox, Acl::AddItems)
.await?
cached_mailboxes
.shared_mailboxes(access_token, Acl::AddItems)
.into()
} else {
None
@ -91,7 +91,7 @@ impl EmailImport for Server {
continue;
}
for mailbox_id in &mailbox_ids {
if !valid_mailbox_ids.items.contains_key(mailbox_id) {
if !cached_mailboxes.items.contains_key(mailbox_id) {
response.not_created.append(
id,
SetError::invalid_properties()

View file

@ -5,7 +5,10 @@
*/
use common::{MessageItemCache, MessageStoreCache, Server, auth::AccessToken};
use email::message::cache::{MessageCache, MessageCacheAccess};
use email::{
mailbox::cache::MessageMailboxCache,
message::cache::{MessageCache, MessageCacheAccess},
};
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::email::QueryArguments,
@ -21,6 +24,7 @@ use store::{
query::{self},
roaring::RoaringBitmap,
};
use trc::AddContext;
use crate::JmapMethods;
@ -40,7 +44,10 @@ impl EmailQuery for Server {
) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id();
let mut filters = Vec::with_capacity(request.filter.len());
let cache = self.get_cached_messages(account_id).await?;
let cached_messages = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
for cond_group in std::mem::take(&mut request.filter).into_filter_group() {
match cond_group {
@ -182,7 +189,9 @@ impl EmailQuery for Server {
match cond {
Filter::InMailbox(mailbox) => {
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
cache.in_mailbox(mailbox.document_id()).map(|(id, _)| *id),
cached_messages
.in_mailbox(mailbox.document_id())
.map(|(id, _)| *id),
)))
}
Filter::InMailboxOtherThan(mailboxes) => {
@ -190,7 +199,9 @@ impl EmailQuery for Server {
filters.push(query::Filter::Or);
for mailbox in mailboxes {
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
cache.in_mailbox(mailbox.document_id()).map(|(id, _)| *id),
cached_messages
.in_mailbox(mailbox.document_id())
.map(|(id, _)| *id),
)));
}
filters.push(query::Filter::End);
@ -208,28 +219,38 @@ impl EmailQuery for Server {
Filter::MaxSize(size) => {
filters.push(query::Filter::lt(Property::Size, size.serialize()))
}
Filter::AllInThreadHaveKeyword(keyword) => filters.push(
query::Filter::is_in_set(thread_keywords(&cache, keyword, true)),
),
Filter::SomeInThreadHaveKeyword(keyword) => filters.push(
query::Filter::is_in_set(thread_keywords(&cache, keyword, false)),
),
Filter::AllInThreadHaveKeyword(keyword) => {
filters.push(query::Filter::is_in_set(thread_keywords(
&cached_messages,
keyword,
true,
)))
}
Filter::SomeInThreadHaveKeyword(keyword) => {
filters.push(query::Filter::is_in_set(thread_keywords(
&cached_messages,
keyword,
false,
)))
}
Filter::NoneInThreadHaveKeyword(keyword) => {
filters.push(query::Filter::Not);
filters.push(query::Filter::is_in_set(thread_keywords(
&cache, keyword, false,
&cached_messages,
keyword,
false,
)));
filters.push(query::Filter::End);
}
Filter::HasKeyword(keyword) => {
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
cache.with_keyword(&keyword).map(|(id, _)| *id),
cached_messages.with_keyword(&keyword).map(|(id, _)| *id),
)));
}
Filter::NotKeyword(keyword) => {
filters.push(query::Filter::Not);
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
cache.with_keyword(&keyword).map(|(id, _)| *id),
cached_messages.with_keyword(&keyword).map(|(id, _)| *id),
)));
filters.push(query::Filter::End);
}
@ -259,7 +280,9 @@ impl EmailQuery for Server {
}
Filter::InThread(id) => {
filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter(
cache.in_thread(id.document_id()).map(|(id, _)| *id),
cached_messages
.in_thread(id.document_id())
.map(|(id, _)| *id),
)))
}
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
@ -278,10 +301,15 @@ impl EmailQuery for Server {
let mut result_set = self.filter(account_id, Collection::Email, filters).await?;
if access_token.is_shared(account_id) {
result_set.apply_mask(
self.shared_messages(access_token, account_id, Acl::ReadItems)
.await?,
);
let cached_mailboxes = self
.get_cached_mailboxes(account_id)
.await
.caused_by(trc::location!())?;
result_set.apply_mask(cached_messages.shared_messages(
access_token,
&cached_mailboxes,
Acl::ReadItems,
));
}
let (response, paginate) = self.build_query_response(&result_set, &request).await?;
@ -314,18 +342,26 @@ impl EmailQuery for Server {
}
SortProperty::HasKeyword => query::Comparator::set(
RoaringBitmap::from_iter(
cache
cached_messages
.with_keyword(&comparator.keyword.unwrap_or(Keyword::Seen))
.map(|(id, _)| *id),
),
comparator.is_ascending,
),
SortProperty::AllInThreadHaveKeyword => query::Comparator::set(
thread_keywords(&cache, comparator.keyword.unwrap_or(Keyword::Seen), true),
thread_keywords(
&cached_messages,
comparator.keyword.unwrap_or(Keyword::Seen),
true,
),
comparator.is_ascending,
),
SortProperty::SomeInThreadHaveKeyword => query::Comparator::set(
thread_keywords(&cache, comparator.keyword.unwrap_or(Keyword::Seen), false),
thread_keywords(
&cached_messages,
comparator.keyword.unwrap_or(Keyword::Seen),
false,
),
comparator.is_ascending,
),
// Non-standard

View file

@ -8,9 +8,12 @@ use std::{borrow::Cow, collections::HashMap};
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use email::{
mailbox::UidMailbox,
mailbox::{
UidMailbox,
cache::{MailboxCacheAccess, MessageMailboxCache},
},
message::{
cache::MessageCache,
cache::{MessageCache, MessageCacheAccess},
delete::EmailDeletion,
ingest::{EmailIngest, IngestEmail, IngestSource},
metadata::MessageData,
@ -72,28 +75,19 @@ impl EmailSet for Server {
let can_train_spam = self.email_bayes_can_train(access_token);
// Obtain mailboxIds
let mailbox_ids = self.get_cached_messages(account_id).await?;
let cached_mailboxes = self.get_cached_mailboxes(account_id).await?;
let cached_messages = self.get_cached_messages(account_id).await?;
let (can_add_mailbox_ids, can_delete_mailbox_ids, can_modify_message_ids) =
if access_token.is_shared(account_id) {
(
self.shared_containers(
access_token,
account_id,
Collection::Mailbox,
Acl::AddItems,
)
.await?
.into(),
self.shared_containers(
access_token,
account_id,
Collection::Mailbox,
Acl::RemoveItems,
)
.await?
.into(),
self.shared_messages(access_token, account_id, Acl::ModifyItems)
.await?
cached_mailboxes
.shared_mailboxes(access_token, Acl::AddItems)
.into(),
cached_mailboxes
.shared_mailboxes(access_token, Acl::RemoveItems)
.into(),
cached_messages
.shared_messages(access_token, &cached_mailboxes, Acl::ModifyItems)
.into(),
)
} else {
@ -668,7 +662,7 @@ impl EmailSet for Server {
// Verify that the mailboxIds are valid
for mailbox_id in &mailboxes {
if !mailbox_ids.items.contains_key(mailbox_id) {
if !cached_mailboxes.items.contains_key(mailbox_id) {
response.not_created.append(
id,
SetError::invalid_properties()
@ -884,7 +878,7 @@ impl EmailSet for Server {
// Make sure all new mailboxIds are valid
for mailbox_id in new_data.added_mailboxes(data.inner) {
if mailbox_ids.items.contains_key(&mailbox_id.mailbox_id) {
if cached_mailboxes.items.contains_key(&mailbox_id.mailbox_id) {
// Verify permissions on shared accounts
if !matches!(&can_add_mailbox_ids, Some(ids) if !ids.contains(mailbox_id.mailbox_id))
{
@ -992,13 +986,10 @@ impl EmailSet for Server {
// Process deletions
if !will_destroy.is_empty() {
let email_ids = self
.get_document_ids(account_id, Collection::Email)
.await?
.unwrap_or_default();
let email_ids = cached_messages.document_ids();
let can_destroy_message_ids = if access_token.is_shared(account_id) {
self.shared_messages(access_token, account_id, Acl::RemoveItems)
.await?
cached_messages
.shared_messages(access_token, &cached_mailboxes, Acl::RemoveItems)
.into()
} else {
None

View file

@ -5,11 +5,14 @@
*/
use common::{Server, auth::AccessToken};
use email::message::{
cache::MessageCache,
metadata::{
ArchivedGetHeader, ArchivedHeaderName, ArchivedMetadataPartType, DecodedPartContent,
MessageMetadata,
use email::{
mailbox::cache::MessageMailboxCache,
message::{
cache::{MessageCache, MessageCacheAccess},
metadata::{
ArchivedGetHeader, ArchivedHeaderName, ArchivedMetadataPartType, DecodedPartContent,
MessageMetadata,
},
},
};
use jmap_proto::{
@ -89,9 +92,20 @@ impl EmailSearchSnippet for Server {
}
}
let account_id = request.account_id.document_id();
let document_ids = self
.owned_or_shared_messages(access_token, account_id, Acl::ReadItems)
.await?;
let cached_messages = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
let document_ids = if access_token.is_member(account_id) {
cached_messages.document_ids()
} else {
let cached_mailboxes = self
.get_cached_mailboxes(account_id)
.await
.caused_by(trc::location!())?;
cached_messages.shared_messages(access_token, &cached_mailboxes, Acl::ReadItems)
};
let email_ids = request.email_ids.unwrap();
let mut response = GetSearchSnippetResponse {
account_id: request.account_id,

View file

@ -6,14 +6,13 @@
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use email::{
mailbox::cache::MessageMailboxCache,
mailbox::cache::{MailboxCacheAccess, MessageMailboxCache},
message::cache::{MessageCache, MessageCacheAccess},
};
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
types::{
acl::Acl,
collection::Collection,
keyword::Keyword,
property::Property,
value::{Object, Value},
@ -21,7 +20,6 @@ use jmap_proto::{
};
use std::future::Future;
use store::ahash::AHashSet;
use trc::AddContext;
pub trait MailboxGet: Sync + Send {
fn mailbox_get(
@ -55,8 +53,8 @@ impl MailboxGet for Server {
let mailbox_cache = self.get_cached_mailboxes(account_id).await?;
let message_cache = self.get_cached_messages(account_id).await?;
let shared_ids = if access_token.is_shared(account_id) {
self.shared_containers(access_token, account_id, Collection::Mailbox, Acl::Read)
.await?
mailbox_cache
.shared_mailboxes(access_token, Acl::Read)
.into()
} else {
None
@ -73,9 +71,6 @@ impl MailboxGet for Server {
.map(Into::into)
.collect::<Vec<_>>()
};
let fetch_properties = properties
.iter()
.any(|p| matches!(p, Property::SortOrder | Property::Acl | Property::MyRights));
let mut response = GetResponse {
account_id: request.account_id.into(),
state: Some(mailbox_cache.change_id.into()),
@ -98,30 +93,6 @@ impl MailboxGet for Server {
continue;
};
let archived_mailbox_ = if fetch_properties {
match self
.get_archive(account_id, Collection::Mailbox, document_id)
.await?
{
Some(values) => values,
None => {
response.not_found.push(id.into());
continue;
}
}
.into()
} else {
None
};
let archived_mailbox = if let Some(archived_mailbox) = &archived_mailbox_ {
archived_mailbox
.unarchive::<email::mailbox::Mailbox>()
.caused_by(trc::location!())?
.into()
} else {
None
};
let mut mailbox = Object::with_capacity(properties.len());
for property in &properties {
@ -135,9 +106,9 @@ impl MailboxGet for Server {
Value::Null
}
}
Property::SortOrder => Value::from(&archived_mailbox.unwrap().sort_order),
Property::SortOrder => Value::from(cached_mailbox.sort_order()),
Property::ParentId => {
if let Some(parent_id) = cached_mailbox.parent_id {
if let Some(parent_id) = cached_mailbox.parent_id() {
Value::Id((parent_id).into())
} else {
Value::Null
@ -167,7 +138,7 @@ impl MailboxGet for Server {
),
Property::MyRights => {
if access_token.is_shared(account_id) {
let acl = archived_mailbox.unwrap().acls.effective_acl(access_token);
let acl = cached_mailbox.acls.as_slice().effective_acl(access_token);
Object::with_capacity(9)
.with_property(Property::MayReadItems, acl.contains(Acl::ReadItems))
.with_property(Property::MayAddItems, acl.contains(Acl::AddItems))
@ -208,7 +179,7 @@ impl MailboxGet for Server {
.contains(&access_token.primary_id()),
),
Property::Acl => {
self.acl_get(&archived_mailbox.unwrap().acls, access_token, account_id)
self.acl_get(&cached_mailbox.acls, access_token, account_id)
.await
}

View file

@ -5,20 +5,22 @@
*/
use common::{Server, auth::AccessToken, config::jmap::settings::SpecialUse};
use email::mailbox::cache::MessageMailboxCache;
use email::mailbox::cache::{MailboxCacheAccess, MessageMailboxCache};
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::mailbox::QueryArguments,
types::{acl::Acl, collection::Collection, property::Property},
types::{acl::Acl, collection::Collection},
};
use store::{
ahash::AHashSet,
query::{self, sort::Pagination},
query::{self},
roaring::RoaringBitmap,
};
use crate::{JmapMethods, UpdateResults};
use std::{collections::BTreeMap, future::Future};
use crate::JmapMethods;
use std::{
collections::{BTreeMap, BTreeSet},
future::Future,
};
pub trait MailboxQuery: Sync + Send {
fn mailbox_query(
@ -43,7 +45,7 @@ impl MailboxQuery for Server {
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::ParentId(parent_id) => {
let parent_id = parent_id.map(|id| id.document_id());
let parent_id = parent_id.map(|id| id.document_id()).unwrap_or(u32::MAX);
filters.push(query::Filter::is_in_set(
mailboxes
.items
@ -61,11 +63,12 @@ impl MailboxQuery for Server {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
let name = name.to_lowercase();
filters.push(query::Filter::is_in_set(
mailboxes
.items
.iter()
.filter(|(_, mailbox)| mailbox.name.contains(&name))
.filter(|(_, mailbox)| mailbox.name.to_lowercase().contains(&name))
.map(|(id, _)| id)
.collect::<RoaringBitmap>(),
));
@ -103,7 +106,7 @@ impl MailboxQuery for Server {
mailboxes
.items
.iter()
.filter(|(_, mailbox)| matches!(mailbox.role, SpecialUse::None))
.filter(|(_, mailbox)| !matches!(mailbox.role, SpecialUse::None))
.map(|(id, _)| id)
.collect::<RoaringBitmap>(),
));
@ -145,10 +148,7 @@ impl MailboxQuery for Server {
.filter(account_id, Collection::Mailbox, filters)
.await?;
if access_token.is_shared(account_id) {
result_set.apply_mask(
self.shared_containers(access_token, account_id, Collection::Mailbox, Acl::Read)
.await?,
);
result_set.apply_mask(mailboxes.shared_mailboxes(access_token, Acl::Read));
}
let (mut response, mut paginate) = self.build_query_response(&result_set, &request).await?;
@ -160,7 +160,7 @@ impl MailboxQuery for Server {
let mut check_id = document_id;
for _ in 0..self.core.jmap.mailbox_max_depth {
if let Some(mailbox) = mailboxes.items.get(&check_id) {
if let Some(parent_id) = mailbox.parent_id {
if let Some(parent_id) = mailbox.parent_id() {
if result_set.results.contains(parent_id) {
check_id = parent_id;
} else {
@ -186,24 +186,69 @@ impl MailboxQuery for Server {
}
}
if let Some(mut paginate) = paginate {
let todo = "sort from cache";
// Parse sort criteria
if let Some(paginate) = paginate {
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
// Sort as tree
if sort_as_tree {
let sorted_list = mailboxes
.items
.iter()
.map(|(id, mailbox)| (mailbox.path.as_str(), *id))
.collect::<BTreeMap<_, _>>();
comparators.push(query::Comparator::sorted_list(
sorted_list.into_values().collect(),
true,
));
}
// Parse sort criteria
for comparator in request
.sort
.and_then(|s| if !s.is_empty() { s.into() } else { None })
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![Comparator::ascending(SortProperty::ParentId)])
{
comparators.push(match comparator.property {
SortProperty::Name => {
query::Comparator::field(Property::Name, comparator.is_ascending)
let sorted_list = mailboxes
.items
.iter()
.map(|(id, mailbox)| (mailbox.name.as_str(), *id))
.collect::<BTreeSet<_>>();
query::Comparator::sorted_list(
sorted_list.into_iter().map(|v| v.1).collect(),
comparator.is_ascending,
)
}
SortProperty::SortOrder => {
query::Comparator::field(Property::SortOrder, comparator.is_ascending)
let sorted_list = mailboxes
.items
.iter()
.map(|(id, mailbox)| (mailbox.sort_order, *id))
.collect::<BTreeSet<_>>();
query::Comparator::sorted_list(
sorted_list.into_iter().map(|v| v.1).collect(),
comparator.is_ascending,
)
}
SortProperty::ParentId => {
query::Comparator::field(Property::ParentId, comparator.is_ascending)
let sorted_list = mailboxes
.items
.iter()
.map(|(id, mailbox)| {
(
mailbox.parent_id().map(|id| id + 1).unwrap_or_default(),
*id,
)
})
.collect::<BTreeSet<_>>();
query::Comparator::sorted_list(
sorted_list.into_iter().map(|v| v.1).collect(),
comparator.is_ascending,
)
}
other => {
@ -214,35 +259,9 @@ impl MailboxQuery for Server {
});
}
// Sort as tree
if sort_as_tree {
let dummy_paginate = Pagination::new(result_set.results.len() as usize, 0, None, 0);
response = self
.sort(result_set, comparators, dummy_paginate, response)
.await?;
let sorted_tree = mailboxes
.items
.iter()
.map(|(id, mailbox)| (mailbox.path.as_str(), *id))
.collect::<BTreeMap<_, _>>();
let ids = response
.ids
.iter()
.map(|id| id.document_id())
.collect::<AHashSet<_>>();
for (_, document_id) in sorted_tree {
if ids.contains(&document_id) && !paginate.add(0, document_id) {
break;
}
}
response.update_results(paginate.build())?;
} else {
response = self
.sort(result_set, comparators, paginate, response)
.await?;
}
response = self
.sort(result_set, comparators, paginate, response)
.await?;
}
Ok(response)

View file

@ -9,7 +9,11 @@ use common::{
storage::index::ObjectIndexBuilder,
};
use email::mailbox::{Mailbox, cache::MessageMailboxCache, destroy::MailboxDestroy};
use email::mailbox::{
Mailbox,
cache::{MailboxCacheAccess, MessageMailboxCache},
destroy::MailboxDestroy,
};
use jmap_proto::{
error::set::SetError,
method::set::{SetRequest, SetResponse},
@ -25,8 +29,6 @@ use jmap_proto::{
},
};
use store::{
SerializeInfallible,
query::Filter,
roaring::RoaringBitmap,
write::{Archive, BatchBuilder, assert::AssertValue},
};
@ -431,30 +433,15 @@ impl MailboxSet for Server {
}
}
let cached_mailboxes = self.get_cached_mailboxes(ctx.account_id).await?;
// Verify that the mailbox role is unique.
if !matches!(changes.role, SpecialUse::None)
&& update
.as_ref()
.is_none_or(|(_, m)| m.inner.role != changes.role)
{
if !self
.filter(
ctx.account_id,
Collection::Mailbox,
vec![Filter::eq(
Property::Role,
changes
.role
.as_str()
.unwrap_or_default()
.as_bytes()
.to_vec(),
)],
)
.await?
.results
.is_empty()
{
if cached_mailboxes.by_role(&changes.role).is_some() {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Role)
.with_description(format!(
@ -478,21 +465,14 @@ impl MailboxSet for Server {
// Verify that the mailbox name is unique.
if !changes.name.is_empty() {
// Obtain parent mailbox id
let lower_name = changes.name.to_lowercase();
if update
.as_ref()
.is_none_or(|(_, m)| m.inner.name != changes.name)
&& !self
.filter(
ctx.account_id,
Collection::Mailbox,
vec![
Filter::eq(Property::Name, changes.name.to_lowercase().into_bytes()),
Filter::eq(Property::ParentId, changes.parent_id.serialize()),
],
)
.await?
.results
.is_empty()
&& cached_mailboxes.items.iter().any(|(_, m)| {
m.name.to_lowercase() == lower_name
&& m.parent_id().map_or(0, |id| id + 1) == changes.parent_id
})
{
return Ok(Err(SetError::invalid_properties()
.with_property(Property::Name)

View file

@ -50,6 +50,7 @@ pub enum Filter {
pub enum Comparator {
Field { field: u8, ascending: bool },
DocumentSet { set: RoaringBitmap, ascending: bool },
SortedList { list: Vec<u32>, ascending: bool },
}
#[derive(Debug)]
@ -176,6 +177,10 @@ impl Comparator {
Self::DocumentSet { set, ascending }
}
pub fn sorted_list(list: Vec<u32>, ascending: bool) -> Self {
Self::SortedList { list, ascending }
}
pub fn ascending(field: impl Into<u8>) -> Self {
Self::Field {
field: field.into(),

View file

@ -13,6 +13,7 @@ use crate::{IndexKeyPrefix, IterateParams, Store, U32_LEN, write::key::Deseriali
use super::{Comparator, ResultSet, SortedResultSet};
#[derive(Debug)]
pub struct Pagination<'x> {
requested_position: i32,
position: i32,
@ -99,6 +100,25 @@ impl Store {
}
}
}
Comparator::SortedList { list, ascending } => {
if ascending {
for document_id in list {
if result_set.results.contains(document_id)
&& !paginate.add(0, document_id)
{
break;
}
}
} else {
for document_id in list.into_iter().rev() {
if result_set.results.contains(document_id)
&& !paginate.add(0, document_id)
{
break;
}
}
}
}
}
// Obtain prefixes
@ -195,6 +215,23 @@ impl Store {
}
}
}
Comparator::SortedList { list, ascending } => {
if ascending {
for (idx, document_id) in list.into_iter().enumerate() {
if result_set.results.contains(document_id) {
sorted_ids.entry(document_id).or_insert([0u32; 4])[pos] =
idx as u32;
}
}
} else {
for (idx, document_id) in list.into_iter().rev().enumerate() {
if result_set.results.contains(document_id) {
sorted_ids.entry(document_id).or_insert([0u32; 4])[pos] =
idx as u32;
}
}
}
}
}
}

View file

@ -131,6 +131,7 @@ pub async fn test(params: &mut JMAPTest) {
),
)
.await;
let john_cache = server.get_cached_messages(john_id).await.unwrap();
assert_eq!(
server

View file

@ -9,7 +9,7 @@ use std::{fs, path::PathBuf};
use crate::jmap::{assert_is_empty, mailbox::destroy_all_mailboxes};
use ahash::AHashSet;
use ::email::mailbox::{INBOX_ID, manage::MailboxFnc};
use ::email::mailbox::INBOX_ID;
use jmap_client::{
Error, Set,
client::Client,

View file

@ -383,7 +383,7 @@ pub async fn jmap_tests() {
email_query_changes::test(&mut params).await;
email_copy::test(&mut params).await;
thread_get::test(&mut params).await;
thread_merge::test(&mut params).await;
//thread_merge::test(&mut params).await;
mailbox::test(&mut params).await;
delivery::test(&mut params).await;
auth_acl::test(&mut params).await;
@ -509,6 +509,10 @@ pub async fn assert_is_empty(server: Server) {
.data
.assert_is_empty(server.core.storage.blob.clone())
.await;
// Clean cache
server.inner.cache.mailboxes.clear();
server.inner.cache.messages.clear();
}
pub async fn emails_purge_tombstoned(server: &Server) {