Skip to content

mailsec: --reason on message bulk-action (+ SDK parameter) - #354

Merged
maximelb merged 1 commit into
masterfrom
mailsec/bulk-action-reason
Sep 2, 2026
Merged

mailsec: --reason on message bulk-action (+ SDK parameter)#354
maximelb merged 1 commit into
masterfrom
mailsec/bulk-action-reason

Conversation

@maximelb

@maximelb maximelb commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Requires the backend PRs to be merged and deployed first. Without them the reason is
accepted here and discarded in transit. Merge order is backend → gateway → this, last.
Draft, and not to be merged before the other two ship.

Why the SDK said there was no reason

Mailsec.bulk_action_execute's docstring carried this note:

There is deliberately no reason. The gateway forwards only action, msg_uuids,
confirm, attempt and the banner on this route, so a reason would be accepted by this
client and dropped in transit — a justification that silently never reaches the audit trail
is worse than one the caller knows it cannot give.

That was the right call against the backend as it stood. The backend changes it: the gateway
now forwards reason, and the collector records it on the job's audit row and on every
message's — the same record act_on_message(..., reason=...) produces for one message.

The change

  • Mailsec.bulk_action_execute(..., reason=None), sent only when supplied (the same
    omit-when-absent discipline every other optional on these routes uses).
  • limacharlie mailsec message bulk-action --reason, passed on the execute.
  • The docstring note is replaced with an accurate one: reason needs a backend that reads
    it, and a client cannot detect an older one from the response — the execute answers 200
    either way, which is exactly why the old note existed.

The preview still takes no reason, on purpose

The preview mints the confirmation, and the backend derives that token from
(oid, action, attempt, members). A reason that reached the preview could end up in the
derivation, and then rewording a justification would invalidate a selection somebody had
already approved — and mint a second bulk id, i.e. a second job over the same 500
messages instead of the idempotent adoption the design guarantees.

So --reason without --confirm prints a note saying it applies to the execute, rather
than accepting it quietly. A justification silently discarded is worse than a refused one,
because the caller believes it was recorded — which is the entire failure this PR exists to
end, and it would be embarrassing to reintroduce it one layer up.

Tests

tests/unit/test_sdk_mailsec.py

  • test_execute_carries_the_operator_reason
  • test_the_preview_takes_no_reason — the structural half, asserted as a TypeError.

tests/unit/test_cli_mailsec_bulk.py

  • test_the_reason_reaches_the_execute
  • test_a_reason_on_a_preview_is_said_out_loud_not_dropped, which also asserts the reason
    did not travel on the preview call.

pytest tests/unit/3996 passed, 6 skipped (the same 6 skips as origin/master; my
four are the delta from 3992).

Not included

No CHANGELOG.md entry and no version bump: the changelog here is organized per released
version with no Unreleased section, and choosing the version this lands in is a release
decision rather than part of the change.

…ind it

Single-message remediation has always taken a reason; the bulk route did not,
because the API gateway's execute forwarder dropped the field. With the backend
change that reads and records it, the client can offer it.

- Mailsec.bulk_action_execute takes an optional `reason`, sent only when given.
- `mailsec message bulk-action --reason` passes it on the EXECUTE.
- The preview deliberately takes no reason: it mints the confirmation, which the
  backend derives from (oid, action, attempt, members), and a reason that reached
  it could enter that derivation — rewording a justification would then invalidate
  a selection somebody had already approved and start a second job over the same
  messages. Passing --reason without --confirm therefore prints a note rather than
  dropping it silently.

The SDK docstring's "there is deliberately no reason" note is replaced with an
accurate one: it requires a backend that reads the field, and a client cannot
detect an older one from the response, which answers 200 either way.

Requires the backend changes to be deployed first; without them the reason is
accepted here and discarded in transit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@maximelb
maximelb marked this pull request as ready for review September 2, 2026 16:25
@maximelb
maximelb enabled auto-merge (squash) September 2, 2026 16:25
@maximelb
maximelb merged commit dd35843 into master Sep 2, 2026
6 checks passed
@maximelb
maximelb deleted the mailsec/bulk-action-reason branch September 2, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants