Masked email: x:MaskedEmail/changes and a state in /get (fork additions)

The fork's per-account change log answers /changes, collapsing a mask
created and destroyed in the window. /changes on a registry type needs that
type's get permission.
This commit is contained in:
2026-09-18 16:25:54 -07:00
parent 53ccc8f4de
commit 7080028437
9 changed files with 170 additions and 11 deletions
+3 -2
View File
@@ -247,8 +247,9 @@ impl JmapAuthorization for AccessToken {
| MethodObject::PushSubscription
| MethodObject::SearchSnippet
| MethodObject::VacationResponse
| MethodObject::SieveScript
| MethodObject::Registry(_) => Permission::JmapEmailChanges,
| MethodObject::SieveScript => Permission::JmapEmailChanges,
// inbuxa: x:MaskedEmail/changes reads what /get reads
MethodObject::Registry(object_type) => object_type.get_permission(),
},
RequestMethod::Copy(m) => match &m {
CopyRequestMethod::Email(_) => Permission::JmapEmailCopy,
+8 -3
View File
@@ -592,9 +592,14 @@ impl RequestHandler for Server {
RequestMethod::Changes(mut req) => {
resolve_account_id(&mut req.account_id, method_name.obj, access_token)?;
self.changes(*req, method_name.obj, access_token)
.await?
.into_method_response()
// inbuxa: x:MaskedEmail/changes
if matches!(method_name.obj, MethodObject::Registry(_)) {
crate::inbuxa::masked_email::changes(self, access_token, *req).await?
} else {
self.changes(*req, method_name.obj, access_token)
.await?
.into_method_response()
}
}
RequestMethod::Copy(req) => match req {
CopyRequestMethod::Email(mut req) => {
+64
View File
@@ -350,3 +350,67 @@ pub async fn query(mut req: RegistryQueryResponse<'_>) -> trc::Result<QueryRespo
}
Ok(response)
}
/// The state of an account's masks, for `/get` and `/changes` (a fork
/// addition: upstream's `/get` has none).
pub async fn state(server: &Server, account_id: u32) -> trc::Result<JmapState> {
let latest =
inbuxa_features::masked_email::data::latest_change(&server.core.storage.data, account_id)
.await?;
Ok(if latest == 0 {
JmapState::Initial
} else {
JmapState::Exact(latest)
})
}
/// `x:MaskedEmail/changes` (a fork addition).
pub async fn changes(
server: &Server,
access_token: &AccessToken,
request: jmap_proto::method::changes::ChangesRequest,
) -> trc::Result<jmap_proto::response::ResponseMethod<'static>> {
use jmap_proto::{
method::changes::ChangesResponse,
response::{ChangesResponseMethod, ResponseMethod},
};
let account_id = request.account_id.document_id();
assert_can_manage(server, access_token, account_id).await?;
let since = match &request.since_state {
JmapState::Initial => 0,
JmapState::Exact(change_id) => *change_id,
JmapState::Intermediate(_) => {
return Err(trc::JmapEvent::CannotCalculateChanges.into_err());
}
};
let max = request
.max_changes
.filter(|max| *max != 0)
.unwrap_or(usize::MAX)
.min(server.core.jmap.changes_max_results);
let entries = inbuxa_features::masked_email::data::changes_since(
&server.core.storage.data,
account_id,
since,
)
.await?;
let changes = ops::collapse(since, &entries, max);
Ok(ResponseMethod::Changes(ChangesResponseMethod::Registry(
Box::new(ChangesResponse {
account_id: request.account_id,
old_state: request.since_state,
new_state: if changes.new_state == 0 {
JmapState::Initial
} else {
JmapState::Exact(changes.new_state)
},
has_more_changes: changes.has_more,
created: changes.created,
updated: changes.updated,
destroyed: changes.destroyed,
updated_properties: None,
}),
)))
}
+6
View File
@@ -357,6 +357,12 @@ impl RegistryGet for Server {
get.insert(id, object);
}
// inbuxa: a state for masked email (a fork addition)
if object_type == ObjectType::MaskedEmail {
get.response.state =
Some(crate::inbuxa::masked_email::state(self, get.account_id).await?);
}
Ok(get.into_response())
}
ObjectType::QueuedMessage => {