diff --git a/CHANGELOG.md b/CHANGELOG.md index bffcb1b7..b068e84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Fixed - DAV: `acl-principal-prop-set` REPORT enforced the wrong privilege. +- JMAP: `Thread/get` did not filter by per-mailbox ACLs on shared accounts. ## [0.16.5] - 2026-05-11 diff --git a/crates/common/src/network/dns/update.rs b/crates/common/src/network/dns/update.rs index 86173203..74e808cb 100644 --- a/crates/common/src/network/dns/update.rs +++ b/crates/common/src/network/dns/update.rs @@ -47,11 +47,11 @@ impl DnsUpdater { core, updater: dns_update::DnsUpdater::new_rfc2136_tsig( match server.protocol { - enums::IpProtocol::Udp => DnsAddress::Tcp(SocketAddr::new( + enums::IpProtocol::Tcp => DnsAddress::Tcp(SocketAddr::new( server.host.into_inner(), server.port as u16, )), - enums::IpProtocol::Tcp => DnsAddress::Udp(SocketAddr::new( + enums::IpProtocol::Udp => DnsAddress::Udp(SocketAddr::new( server.host.into_inner(), server.port as u16, )), diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 5a7cb288..f391b9ef 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -247,7 +247,7 @@ impl RequestHandler for Server { set_account_id_if_missing(&mut req.account_id, access_token); access_token.assert_has_access(req.account_id, Collection::Email)?; - self.thread_get(*req).await?.into() + self.thread_get(*req, access_token).await?.into() } GetRequestMethod::Identity(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs index 56f673c2..75599c33 100644 --- a/crates/jmap/src/thread/get.rs +++ b/crates/jmap/src/thread/get.rs @@ -5,8 +5,8 @@ */ use crate::changes::state::StateManager; -use common::Server; -use email::cache::MessageCacheFetch; +use common::{Server, auth::AccessToken}; +use email::cache::{MessageCacheFetch, email::MessageCacheAccess}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::thread::{Thread, ThreadProperty, ThreadValue}, @@ -21,12 +21,13 @@ use store::{ write::SearchIndex, }; use trc::AddContext; -use types::{collection::SyncCollection, id::Id}; +use types::{acl::Acl, collection::SyncCollection, id::Id}; pub trait ThreadGet: Sync + Send { fn thread_get( &self, request: GetRequest, + access_token: &AccessToken, ) -> impl Future>> + Send; } @@ -34,17 +35,27 @@ impl ThreadGet for Server { async fn thread_get( &self, mut request: GetRequest, + access_token: &AccessToken, ) -> trc::Result> { let account_id = request.account_id.document_id(); - let mut thread_map: AHashMap = AHashMap::with_capacity(32); - let mut all_ids = RoaringBitmap::new(); - for item in &self + let cache = self .get_cached_messages(account_id) .await - .caused_by(trc::location!())? - .emails - .items - { + .caused_by(trc::location!())?; + let shared_ids = if access_token.is_shared(account_id) { + Some(cache.shared_messages(access_token, Acl::ReadItems)) + } else { + None + }; + let mut thread_map: AHashMap = AHashMap::with_capacity(32); + let mut all_ids = RoaringBitmap::new(); + for item in &cache.emails.items { + if shared_ids + .as_ref() + .is_some_and(|ids| !ids.contains(item.document_id)) + { + continue; + } thread_map .entry(item.thread_id) .or_default()