Skip to content

exmdb, zcore, php_mapi: retroactive rule execution - #282

Open
r4nc0r wants to merge 1 commit into
grommunio:primefrom
r4nc0r:feature/retroactive-rule-execution
Open

exmdb, zcore, php_mapi: retroactive rule execution#282
r4nc0r wants to merge 1 commit into
grommunio:primefrom
r4nc0r:feature/retroactive-rule-execution

Conversation

@r4nc0r

@r4nc0r r4nc0r commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Adds a way to run a folder's rules over messages already in it, rather than only at delivery time: exmdb RPC rules_execute (0x99), zcore RPC rulesexecute (0x5e), and the PHP binding mapi_folder_rulesexecute().

Why

This exists for the grommunio-web "Run rules now" feature (grommunio/grommunio-web#97, implementing grommunio/grommunio-web#86). Because gromox currently exposes no way to execute rules against an existing message, that PR had to reimplement a subset of the rule engine in PHP — roughly 368 lines in class.rulesmodule.php that walk a folder, evaluate each rule's condition as a table restriction, and dispatch the actions by hand.

That duplication is the problem this addresses. The PHP executor covers OP_MOVE, OP_COPY, OP_DELETE, OP_MARK_AS_READ and OP_TAG, and reports forward/reply/bounce/deferred/delegate/OOF as skipped, because it cannot reasonably implement them. gromox's own Onestep engine already implements all of them. Exposing the engine lets the web client's server side be reduced to a thin call, removes a second rule executor that has to stay in agreement with this one, and makes the remaining action types work rather than be skipped.

Approach

message_rule_new_message() is reused as-is rather than reimplemented; the delivery entry point and the new one now share a single rule_exec_common(), with exmdb_server::rule_new_message() a thin wrapper that behaves exactly as before.

Retroactive execution differs from delivery in three ways, all of which follow from the message pre-existing rather than having just arrived.

The (folder, message) pair is verified against the store. folder_id only selects which rule set is loaded, while every action addresses the message by id alone — the OP_DELETE path is a bare DELETE FROM messages WHERE message_id=, and cu_copy_message() takes the mid. Entryids are unauthenticated and carry the two GC values independently, so without this check a caller holding rights on one folder could run its rules against a message in another. The same query excludes soft-deleted and FAI messages, since neither is a legitimate target — FAI notably includes the extended-rule messages themselves.

Messages the rules mutate or delete get a change number and notifications. Delivery deliberately omits both, because nothing has ever observed a newly-arrived message; that assumption does not hold here, and without this a retroactive move leaves the message visible in both folders on every connected client, and a retroactive mark-as-read never reaches other devices via ICS.

An action-class gate (enum rule_action_flags) decides which classes may run at all. The default, RX_ACT_SAFE, permits only reversible actions. RX_ACT_DELETE and RX_ACT_SEND have to be requested by name, because a rule's OP_DELETE is a hard delete that does not pass through Deleted Items, and replaying reply/forward/bounce/delegate over an archive emits mail — possibly years late and once per matching message. Delivery is unaffected: it passes RX_ACT_ALL, and the gate defaults to permitting everything, so no existing call site changes behaviour.

zcore layer

zs_rulesexecute() requires frightsOwner on the folder, takes an entryid batch rather than a whole folder, caps the batch, and deduplicates message ids so a repeated entryid cannot amplify a single OP_FORWARD rule into many outbound mails. Per-message failures do not abort the batch — since the actions already applied cannot be rolled back — and the call returns processed, skipped and failed so the caller can report all three.

The batch is the caller's unit of work by design: zcore holds the per-user session lock for a whole request, so a folder-wide variant would stall that user's other requests, including notification polling, for the duration.

Notes for review

Two points I would rather have a maintainer's opinion on than decide unilaterally.

A delegate holding frightsOwner on a folder can author a forwarding rule and then trigger it in bulk over the whole folder. This is amplification of something already reachable via zs_modifyrules plus inbound mail, not a new capability, but the retroactive call turns a slow drip into a single request, and it may warrant an audit log line or a tighter right.

The batch cap is currently 1000. That is a large number for a loop that holds the session lock, particularly when RX_ACT_SEND makes each iteration a potential SMTP submission; a lower cap, or a separate lower cap when RX_ACT_SEND is set, may be the better default.

Add exmdb rules_execute (0x99) and zcore rulesexecute (0x5e), exposed as
mapi_folder_rulesexecute(), to run a folder's rules over messages already in
it. Reuses the Onestep engine rather than reimplementing action dispatch.

Retroactive execution differs from delivery in three ways, all because the
message pre-exists: the (folder, message) pair is verified, since folder_id
only selects the rule set while every action addresses the message by id;
mutated and deleted messages get a change number and notifications, which
delivery omits because nothing has observed a new message yet; and
action_flags gates which action classes may run.

RX_ACT_SAFE (the default) permits only reversible actions. Hard delete and
the sending actions -- reply, forward, bounce, delegate -- must be requested
by name, since replaying them over an archive destroys mail or emits it,
possibly years late and once per message.

zcore caps the batch, deduplicates message ids so a repeated entryid cannot
amplify one forward rule into many, and reports processed/skipped/failed
without aborting the batch on a single bad entryid.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant