From d93c23f6282d5545d959e3990afa8d004f810a5f Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 00:01:51 +0000 Subject: [PATCH 1/6] Document independent dispositions, report replies and release workflows --- docs/email-security/api-reference.md | 20 +++++++- docs/email-security/automation.md | 20 +++++++- docs/email-security/cli.md | 30 ++++++++++- docs/email-security/messages.md | 47 ++++++++++++++++++ docs/email-security/policy.md | 10 ++-- docs/email-security/rule-reference.md | 1 + docs/email-security/user-reports.md | 71 ++++++++++++++++----------- 7 files changed, 165 insertions(+), 34 deletions(-) diff --git a/docs/email-security/api-reference.md b/docs/email-security/api-reference.md index 430998a34..1924d2657 100644 --- a/docs/email-security/api-reference.md +++ b/docs/email-security/api-reference.md @@ -312,7 +312,7 @@ faster, which is worth doing for its own sake. | Route | Does | |---|---| -| `POST /messages/{msg_uuid}/actions` | Perform a typed action on one message. Body: `action` (`quarantine_message`, `trash_message`, `move_to_spam`, `restore_message`, `banner_message`, `unbanner_message`), optional `force` (boolean; see [alert-only overrides](#explicit-override-in-alert-only-mode)), optional `reason`, optional `attempt` (idempotency token — omit to collapse onto the existing attempt). `banner_message` uses the organization's own banner, rendered from its `mailsec_policy` record of type `banners`; its optional `text` (plain text, at most 512 characters; refused on any other action) replaces the wording for that one banner. No caller supplies HTML. Requires `mailsec.act` | +| `POST /messages/{msg_uuid}/actions` | Perform a typed action on one message. Body: `action` (`quarantine_message`, `trash_message`, `move_to_spam`, `restore_message`, `release_message`, `banner_message`, `unbanner_message`), optional `force` (boolean; see [alert-only overrides](#explicit-override-in-alert-only-mode)), optional `reason`, optional `attempt` (idempotency token — omit to collapse onto the existing attempt). `banner_message` uses the organization's own banner, rendered from its `mailsec_policy` record of type `banners`; its optional `text` (plain text, at most 512 characters; refused on any other action) replaces the wording for that one banner. No caller supplies HTML. Requires `mailsec.act` | | `POST /messages/{msg_uuid}/actions` with `submit_sample` or `withdraw_sample` | Copy one message to LimaCharlie, or withdraw that copy. `submit_sample` requires `category` (`missed_threat`, `false_positive`, `other`) and a `reason` of 1-1024 characters; `withdraw_sample` takes an optional `reason`. Only a person can run either: D&R rules, automations and the AI agent are refused. Submission requires organization opt-in; withdrawal remains available after opt-out or provider disconnect. See [Sample Submission](sample-submission.md). Requires `mailsec.act` | | `DELETE /submissions/{submission_id}` | Withdraw a submission: hard-deletes the stored copy and its metadata and returns `{withdrawn: true, submission_id, action_id}`. An unknown or already-deleted id returns `{withdrawn: false, submission_id}` with no `action_id`; an expired id is still cleaned up if metadata remains. Requires `mailsec.act` | | `POST /campaigns/{campaign_id}/actions` | Sweep a campaign. Same body plus `confirm`. **Without `confirm` this previews** and changes nothing, returning the member ids, the distinct mailboxes, the counts and a `confirm` token derived from that exact member set. With `confirm` it executes exactly that set; a campaign that grew since the preview is refused. Capped at 500 members. `reason` is recorded on **every member's** audit row and on the sweep's own row (`action_id` in the response); `attempt` (bounded at 128 characters, refused not truncated) mints a new row per member, so a deliberate retry is recorded beside what it retried instead of over it. Neither is part of the `confirm` token. Requires `mailsec.act` | @@ -484,6 +484,8 @@ telemetry and every customer rule is keyed on them. | `EMAIL_VERDICT` | Once per verdict **decision**. `revision/seq: 0` with `revision/mode: auto` is the rule pack's own verdict, emitted at ingest immediately after that message's `EMAIL_MESSAGE`; `seq: 1…` is one per override (`analyst`, `ai`, `detonation`) | | `EMAIL_ACTION` | Once per remediation outcome, including failures and skips | | `EMAIL_USER_REPORT` | Once per message that reaches the abuse mailbox | +| `EMAIL_DISPOSITION` | Independent analyst/SOAR disposition changed or cleared; carries actor, source, note, server timestamp, prior value, and sequence | +| `EMAIL_REPORT_RESOLVED` | Report resolved; carries report/message identities, recorded disposition and resolver, and mailbox when available | | `EMAIL_INGEST_ERROR` | Once per message that could not be fetched or processed | Two consequences worth stating plainly: @@ -525,3 +527,19 @@ release status values are `requested`, `released` and `denied`. Responses includ independent per-feed `coverage`; an empty list with `not_granted`, pending, stale or error coverage is not proof of zero blocked messages. See [Provider Quarantine](provider-quarantine.md#cli-and-api) for the row contract. + +### Disposition and release + +| Route | Body and behavior | +|---|---| +| `POST /messages/{msg_uuid}/disposition` | `disposition` from the five-value vocabulary, optional `note`; or `clear: true`. Requires `mailsec.set`. | +| `POST /messages/dispositions` | Same decision plus 1–500 unique `msg_uuids`. Returns per-message results, including partial failures. Requires `mailsec.set`. | +| `GET /messages?disposition=` | Filter by one disposition, or `none` for no current label. | +| `POST /messages/{msg_uuid}/actions` with `action: release_message` | Restore, benign verdict revision, benign disposition, history repair. Optional `mode` (`analyst` or `ai`), `reason`, `force`, `attempt`. Requires `mailsec.act`. | +| `POST /reports/{report_id}/resolve` | `disposition`; optional `remediation` with `scope` (`message`, `group` or `campaign`), `action` and optional `confirm`, `reason`, `force`, `attempt` (a required UUID for group scope, reused for preview, confirmation and polling; see [Message Groups](groups.md)). Without confirm, remediation is previewed and the report stays open. Pure resolution requires `mailsec.set`; remediation also requires `mailsec.act`. | + +The five dispositions are `malicious`, `spam`, `graymail`, `benign`, and +`simulation`. Disposition is separate from verdict. Message detail includes +`disposition_info: {disposition, note, actor, source, ts}` and `disposition_seq`. +Source is `analyst`, `api`, `extension`, `report`, or `ai`; attribution and time +are assigned by the service. Bodies cannot forge them. diff --git a/docs/email-security/automation.md b/docs/email-security/automation.md index 19b2b3097..378af7cc9 100644 --- a/docs/email-security/automation.md +++ b/docs/email-security/automation.md @@ -77,6 +77,8 @@ is your abuse mailbox. The person who sent the report is `event/reporter`. | `EMAIL_USER_REPORT` | When a message reaches the abuse mailbox and becomes a report | | `EMAIL_PROVIDER_QUARANTINE` | Microsoft reports a quarantined, spam-filtered or failed delivery. `provider_status` distinguishes them; this is provider delivery metadata, not an engine verdict | | `EMAIL_RELEASE_REQUEST` | An end user requests release from Microsoft hosted quarantine. Releases/denials are retained as history and do not emit this event | +| `EMAIL_DISPOSITION` | Independent analyst/SOAR disposition changed or cleared; carries actor, source, note, server timestamp, prior value, and sequence | +| `EMAIL_REPORT_RESOLVED` | Report resolved; carries report/message identities, recorded disposition and resolver, and mailbox when available | | `EMAIL_INGEST_ERROR` | When a message could not be fetched or processed. Coverage honesty: failures are visible, never silent | `EMAIL_MESSAGE` is emitted once and is immutable. When a verdict changes, the @@ -302,7 +304,7 @@ rules: The typed actions available to `extension request` are the same six the console and the CLI use: `quarantine_message`, `trash_message`, `move_to_spam`, -`restore_message`, `banner_message`, `unbanner_message`. They route to the same +`restore_message`, `release_message`, `banner_message`, `unbanner_message`. They route to the same executor, so the organization's `alert_only` / `enforce` mode, the audit row and idempotency all apply unchanged — there is exactly one remediation path in this product. @@ -667,3 +669,19 @@ before notification from delay inside processing. The same pattern can alarm on another present timing field. `analysis_ms` includes the delayed analysis window; `end_to_end_ms` ends at the initial verdict. Check `clock_skew` before interpreting clamped measurements. + +### Feedback events and typed actions + +`EMAIL_DISPOSITION` and `EMAIL_REPORT_RESOLVED` carry top-level `disposition`, +`actor`, `source`, and `ts`. Disposition events also carry `seq` and `prior`. +Resolution events carry `report_id`. Both carry `msg_uuid`, provider, and mailbox +when an indexed message supplies it. An unlinked resolution retains its explicit +report identity; it never invents a mailbox. Durable retries retain `job_id`. + +The Email Security extension exposes `set_disposition`, `revise_verdict`, +`release_message`, and `resolve_report` as typed actions. They use the caller's +permissions and authenticated identity. A D&R rule may record disposition or +request a release; releases obey alert-only/force and retain the action audit. +The revise action takes `mode: analyst|ai`, a verdict, and a nonempty list of +rationale strings. Resolve accepts the same five dispositions and optional +message/campaign remediation with preview/confirm. diff --git a/docs/email-security/cli.md b/docs/email-security/cli.md index 150ac5e3b..fa74f04b9 100644 --- a/docs/email-security/cli.md +++ b/docs/email-security/cli.md @@ -326,7 +326,7 @@ error: `submission get` returns `submission: null` and `submission withdraw` ret ### Revising a verdict is `mailsec.act`, not `mailsec.set` -`message revise` records a human disposition over the scorer's, appending to the +`message revise` records a human verdict revision over the scorer's, appending to the message's history rather than overwriting it. `--rationale` is required and audited — at least one, at most ten, each 280 characters or fewer. @@ -436,3 +436,31 @@ Microsoft delivery and hosted-quarantine activity. They support connection, status, time-window and cursor filters and return independent coverage. They require a CLI build containing these commands and `mailsec.get`. See [Provider Quarantine](provider-quarantine.md#cli-and-api) for examples and limits. + +## Independent disposition and release + +```bash +limacharlie mailsec message disposition --disposition benign --note "Reviewed" +limacharlie mailsec message disposition --clear +limacharlie mailsec message list --disposition none +limacharlie mailsec message bulk-disposition --msg-uuids --msg-uuids --disposition spam +limacharlie mailsec message release --reason "Reviewed as safe" --mode analyst +``` + +Disposition accepts `malicious`, `spam`, `graymail`, `benign`, or `simulation` and +never changes the engine verdict. A bulk selection is limited to 500 unique IDs; +individual failures are reported and cause a nonzero CLI exit. Release restores +placement and records a benign verdict and disposition. It needs `mailsec.act`; +`--force` supplies explicit consent in alert-only mode. See [Messages](messages.md). + +For report remediation, first preview: + +```bash +limacharlie mailsec report resolve --disposition malicious --scope message --action quarantine_message +# all recipient copies of the reported message's group (durable job) +limacharlie mailsec report resolve --disposition malicious --scope group --action quarantine_message --attempt $(uuidgen) +``` + +Read `remediation_preview`, then repeat with `--confirm ` and, when needed, +`--force`. Pure resolution uses `mailsec.set`; remediation also needs `mailsec.act`. +The report remains open during preview or when provider remediation fails. diff --git a/docs/email-security/messages.md b/docs/email-security/messages.md index f0244c94f..882d55ed0 100644 --- a/docs/email-security/messages.md +++ b/docs/email-security/messages.md @@ -486,3 +486,50 @@ A key with no profile means **no history at all**, and the response says so explicitly rather than returning a zeroed profile that would read as a known-but-quiet sender. Keys are lowercased, and a bare address or domain is resolved for you. + +## Analyst disposition + +Disposition records the security team's decision independently of the engine +verdict: `malicious`, `spam`, `graymail`, `benign`, or `simulation`. Setting it +preserves the verdict and runs no automations. The decision includes a note of up +to 1024 characters, authenticated actor, source, and server timestamp. Message +lists expose `disposition`; message detail includes `disposition_info` and +`disposition_seq`. + +```bash +limacharlie mailsec message disposition --disposition spam --note "Reviewed" --oid $OID +limacharlie mailsec message disposition --clear --oid $OID +limacharlie mailsec message list --disposition none --oid $OID +limacharlie mailsec message bulk-disposition --msg-uuids --msg-uuids --disposition malicious --oid $OID +``` + +These writes require `mailsec.set`. Bulk input is limited to 500 unique IDs and +returns a result for every message, including individual failures. The CLI exits +with a failure status when any member fails. The `none` list filter selects +messages with no current disposition, including cleared decisions. + +A benign decision removes this message's credited sender-history flag; malicious +credits it once. The message-count history remains intact. `EMAIL_DISPOSITION` +reports each real decision change, including a clear (empty disposition), with +its sequence and previous value. Event delivery may retry with the same `job_id`; +consumers should deduplicate by that ID or message/sequence. + +## Release a message + +```bash +limacharlie mailsec message release --reason "Reviewed as safe" --oid $OID +``` + +`release_message` restores provider placement and records both a benign verdict +revision and a benign disposition. It repairs sender history and records one +idempotent action. The revision mode is `analyst` by default; an AI caller can +choose `--mode ai`. It requires `mailsec.act` and follows restore's enforcement +rule: in alert-only mode it is recorded and withheld; `--force` is explicit consent +to perform it. The withheld action changes neither verdict nor disposition. + +Use ordinary `restore_message` when you intend only to move the message back +without deciding it is benign. Revising a verdict to benign alone does not restore +mail automatically. + +Disposition writes require an indexed message within the message retention window. +Retained evidence outside that window remains available for backtesting. diff --git a/docs/email-security/policy.md b/docs/email-security/policy.md index 78fe18488..b7d0dad8f 100644 --- a/docs/email-security/policy.md +++ b/docs/email-security/policy.md @@ -736,15 +736,19 @@ The templated acknowledgement sent to someone who reported a message. See ```yaml policy_type: reporter_reply enabled: true +acknowledgement: "Your report was received and is being reviewed." +on_resolve: true templates: - malicious: "Thanks — you were right. We removed that message from every mailbox it reached." - benign: "Thanks for checking. That message is legitimate; no action was needed." + malicious: "Your report has been reviewed and classified as malicious." + benign: "Your report has been reviewed and classified as benign." ``` | Field | Default | | |---|---|---| | `enabled` | `false` | It sends mail on your behalf to your own staff; opt-in | -| `templates` | — | Keyed by verdict. A verdict with no template falls back to a generic acknowledgement, so enabling replies can never leave a reporter with silence | +| `acknowledgement` | Neutral receipt wording | Plain text for the receipt reply; at most 4096 UTF-8 bytes, no markup | +| `on_resolve` | `false` | Send a separate reply after report resolution | +| `templates` | — | Plain-text resolution templates keyed by malicious, spam, graymail, benign, simulation. Missing entries state the recorded disposition; each is at most 4096 UTF-8 bytes | Template keys must be verdicts. Values are plain text (no `<` or `>`), capped at 4096 characters. diff --git a/docs/email-security/rule-reference.md b/docs/email-security/rule-reference.md index f87cecb08..b16b7cce9 100644 --- a/docs/email-security/rule-reference.md +++ b/docs/email-security/rule-reference.md @@ -15,6 +15,7 @@ have different wrappers and validation rules. | `dr-mail`, `post_verdict` only | `verdict/verdict` | | `dr-general` on `EMAIL_MESSAGE` | `event/sender/email/domain/root` | | `dr-general` on `EMAIL_VERDICT` or `EMAIL_ANALYSIS_COMPLETE` | `event/revision/verdict` | +| `dr-general` on `EMAIL_DISPOSITION` or `EMAIL_REPORT_RESOLVED` | `event/disposition` | | Inside `scope` with `path: links` | `href_url/domain/root` | The MDM is the root of a mail rule. Do not add `mdm/` or `event/`. The Hive diff --git a/docs/email-security/user-reports.md b/docs/email-security/user-reports.md index 5e055a342..4c9c61821 100644 --- a/docs/email-security/user-reports.md +++ b/docs/email-security/user-reports.md @@ -73,15 +73,24 @@ limacharlie mailsec report resolve --disposition malicious --oid $OI | Disposition | Meaning | |---|---| -| `malicious` | It was malicious | -| `spam` | Unwanted mail | -| `graymail` | Legitimate bulk or marketing mail | -| `benign` | Legitimate, harmless mail | -| `simulation` | A known training or simulation message | - -Resolving requires `mailsec.set`, **not** `mailsec.act`: it changes triage state -the product owns, and touches nobody's mailbox. That is the line `mailsec.act` -draws. +| `malicious` | Confirmed threat | +| `spam` | Unwanted spam | +| `graymail` | Bulk or promotional mail | +| `benign` | Reviewed as safe | +| `simulation` | Authorized security simulation | + +Resolving sets the linked message's independent disposition with `source: report` +and emits `EMAIL_REPORT_RESOLVED`. It preserves the engine verdict. A report whose +original is unavailable can still be resolved; its coverage gap remains visible. +Pure resolution requires `mailsec.set`. + +To remediate as part of resolution, also hold `mailsec.act`. Choose `--scope +message` or `--scope campaign` and an `--action`. The first request returns +`remediation_preview` and leaves the report open. Read the affected messages and +mailboxes, then repeat the same request with `--confirm `. Failed, withheld, +or interrupted remediation leaves the report open and returns the action outcome. +Campaign remediation is bounded to the existing 500-message sweep limit. A missing +original or campaign is refused rather than guessed. Resolving an already-resolved report succeeds and reports `already_resolved`, so two analysts clicking at once is not an error. @@ -90,7 +99,9 @@ two analysts clicking at once is not an error. Resolving a report as `benign` subtracts that message's contribution from the sender's flagged-history counter. Without it, one wrong flag would keep weighing on every later message from a legitimate correspondent. The repair - runs once per report even if the resolution is retried. + is guarded per message across dispositions, resolutions, and releases, so retries + do not remove another message’s contribution. A `malicious` disposition credits + that message once; neither operation changes the engine verdict. ## Reopening @@ -179,27 +190,31 @@ Off by default: it sends mail on your behalf, to your own staff. ```yaml policy_type: reporter_reply enabled: true +acknowledgement: "Your report was received and is being reviewed." +on_resolve: true templates: - malicious: "Thanks — you were right. We have removed that message from every mailbox it reached." - benign: "Thanks for checking. That message is legitimate; no action was needed." + malicious: "Your report has been reviewed and classified as malicious." + spam: "Your report has been reviewed and classified as spam." + graymail: "Your report has been reviewed and classified as graymail." + benign: "Your report has been reviewed and classified as benign." + simulation: "Your report was an authorized security simulation." ``` -- Templates are keyed by verdict, and a verdict with no template falls back to a - generic acknowledgement — enabling replies can never leave a reporter with - silence. -- Templates are **plain text** (no `<` or `>`), capped in length. The rendering - is fixed in code. -- Sending needs the optional provider capability: `Mail.Send` on Microsoft 365, - `https://mail.google.com/` on Google Workspace. Without it the reply is refused - **by name** rather than silently skipped. -- Replies are **never** sent to an automated sender. A no-reply address either - blackholes it or bounces it straight back into the abuse mailbox, producing a - fresh report and another reply. -- A reply is sent **once per report**, and the acknowledgement carries a marker so - it cannot be re-read as a new report. The loop guard requires both the marker - **and** that the sender is the abuse mailbox, because a header alone is - attacker-controlled — otherwise anyone who had ever received an - acknowledgement could forge one and keep a phish out of the abuse queue. +- `enabled` controls the receipt acknowledgement. Its default wording is neutral; + it never claims a verdict, a resolution, or a completed provider action. +- `on_resolve` controls a separate reply after resolution. Templates are keyed by + **disposition**, not engine verdict; missing templates fall back to the recorded + disposition. Customize wording to state only outcomes your workflow verifies. +- Acknowledgement and templates are plain text, at most 4096 UTF-8 bytes, with no + `<` or `>`. Resolution replies use fixed, escaped rendering. +- Sending needs `Mail.Send` on Microsoft 365 or `https://mail.google.com/` on + Google Workspace. Automated senders never receive either reply. +- Resolution deliveries use a durable claim. A retry of one resolution does not + send another reply; reopening and resolving again records a new resolution. + If a provider-send outcome is uncertain, delivery is marked `ambiguous` and is + not automatically resent. The report detail exposes `resolution_reply_status`. +- Replies carry a loop marker. The guard also checks that the sender is the abuse + mailbox, because a sender-controlled header alone cannot establish a real reply. ## Automating on reports From 2dc7cbcbef200c13cdcac3ed56378314e005b73e Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 00:48:20 +0000 Subject: [PATCH 2/6] Clarify audited automation modes and interactive report resolution --- docs/email-security/automation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/email-security/automation.md b/docs/email-security/automation.md index 378af7cc9..a739229c3 100644 --- a/docs/email-security/automation.md +++ b/docs/email-security/automation.md @@ -682,6 +682,8 @@ The Email Security extension exposes `set_disposition`, `revise_verdict`, `release_message`, and `resolve_report` as typed actions. They use the caller's permissions and authenticated identity. A D&R rule may record disposition or request a release; releases obey alert-only/force and retain the action audit. +Automated revisions and releases require explicit `mode: ai` and preserve the +rule attribution. Report resolution requires an interactive analyst decision. The revise action takes `mode: analyst|ai`, a verdict, and a nonempty list of rationale strings. Resolve accepts the same five dispositions and optional message/campaign remediation with preview/confirm. From 4644bd0e2c10f6a44c92829f632d18fdc620e459 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 12:37:38 +0000 Subject: [PATCH 3/6] Explain group report remediation and resumable resolution --- docs/email-security/automation.md | 2 +- docs/email-security/cli.md | 8 ++++++++ docs/email-security/user-reports.md | 30 ++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/email-security/automation.md b/docs/email-security/automation.md index a739229c3..695bb0652 100644 --- a/docs/email-security/automation.md +++ b/docs/email-security/automation.md @@ -672,7 +672,7 @@ interpreting clamped measurements. ### Feedback events and typed actions -`EMAIL_DISPOSITION` and `EMAIL_REPORT_RESOLVED` carry top-level `disposition`, +`EMAIL_DISPOSITION` and `EMAIL_REPORT_RESOLVED` carry `group_id` when the indexed original has a message group, alongside top-level `disposition`, `actor`, `source`, and `ts`. Disposition events also carry `seq` and `prior`. Resolution events carry `report_id`. Both carry `msg_uuid`, provider, and mailbox when an indexed message supplies it. An unlinked resolution retains its explicit diff --git a/docs/email-security/cli.md b/docs/email-security/cli.md index fa74f04b9..8f38fe35a 100644 --- a/docs/email-security/cli.md +++ b/docs/email-security/cli.md @@ -464,3 +464,11 @@ limacharlie mailsec report resolve --disposition malicious --scope g Read `remediation_preview`, then repeat with `--confirm ` and, when needed, `--force`. Pure resolution uses `mailsec.set`; remediation also needs `mailsec.act`. The report remains open during preview or when provider remediation fails. + + +Group report remediation uses `--scope group` with an explicit UUID `--attempt`, +reused through preview, confirmation and polling. Add `--wait` to wait up to +300 seconds for a complete preview or resolution; timeouts exit with code 2 and +the durable job continues. Resume with the same attempt and confirmation. +See [group report remediation](user-reports.md#remediate-the-same-message-across-recipients) +for the complete workflow. diff --git a/docs/email-security/user-reports.md b/docs/email-security/user-reports.md index 4c9c61821..1029eaa18 100644 --- a/docs/email-security/user-reports.md +++ b/docs/email-security/user-reports.md @@ -85,13 +85,41 @@ original is unavailable can still be resolved; its coverage gap remains visible. Pure resolution requires `mailsec.set`. To remediate as part of resolution, also hold `mailsec.act`. Choose `--scope -message` or `--scope campaign` and an `--action`. The first request returns +message`, `--scope group`, or `--scope campaign` and an `--action`. The first request returns `remediation_preview` and leaves the report open. Read the affected messages and mailboxes, then repeat the same request with `--confirm `. Failed, withheld, or interrupted remediation leaves the report open and returns the action outcome. Campaign remediation is bounded to the existing 500-message sweep limit. A missing original or campaign is refused rather than guessed. +### Remediate the same message across recipients + +`--scope group` targets copies of the reported original delivered to different +recipients. A campaign targets similar messages; a group represents the same +message. Group actions use a durable, paged job that scales beyond 500 messages. +Only the reported original receives the resolution disposition. + +Choose a UUID attempt once, then reuse it through preview, confirmation and +polling. Preparation freezes the recipient selection before returning a token; +messages arriving afterwards are excluded. For example: + +```bash +ATTEMPT=$(python3 -c 'import uuid; print(uuid.uuid4())') +limacharlie mailsec report resolve "$REPORT" --disposition malicious --scope group --action quarantine_message --attempt "$ATTEMPT" --wait --oid "$OID" --output json +# Read remediation_preview.job and take remediation_preview.confirmation as TOKEN. +limacharlie mailsec report resolve "$REPORT" --disposition malicious --scope group --action quarantine_message --attempt "$ATTEMPT" --confirm "$TOKEN" --wait --oid "$OID" --output json +``` + +Keep the action, disposition, reason, force and attempt unchanged when confirming +or resuming. Changing them needs a fresh preview. `--wait` polls for up to 300 +seconds; a timeout exits with code 2, retains the durable job in the response and +leaves the job running. Repeat the same confirmed command to resume. The report +stays open until every selected message succeeds or is already in the requested +state. Failed or withheld members leave it open; retry them through a new attempt +and confirmation. A force override also needs a fresh preview with `--force`. +If you leave after confirming, remediation continues; finish resolution by +resuming the same request. + Resolving an already-resolved report succeeds and reports `already_resolved`, so two analysts clicking at once is not an error. From 4e637c6a9ed605cfd95840aaa73396013ab12907 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 13:00:00 +0000 Subject: [PATCH 4/6] Keep group remediation reference markdown formatting valid --- docs/email-security/cli.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/email-security/cli.md b/docs/email-security/cli.md index 8f38fe35a..6f521eac1 100644 --- a/docs/email-security/cli.md +++ b/docs/email-security/cli.md @@ -465,7 +465,6 @@ Read `remediation_preview`, then repeat with `--confirm ` and, when neede `--force`. Pure resolution uses `mailsec.set`; remediation also needs `mailsec.act`. The report remains open during preview or when provider remediation fails. - Group report remediation uses `--scope group` with an explicit UUID `--attempt`, reused through preview, confirmation and polling. Add `--wait` to wait up to 300 seconds for a complete preview or resolution; timeouts exit with code 2 and From dddadb2664f1d2282271ec18342a9d898653f77a Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 13:42:25 +0000 Subject: [PATCH 5/6] Require fresh group remediation after reopening a report --- docs/email-security/user-reports.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/email-security/user-reports.md b/docs/email-security/user-reports.md index 1029eaa18..daf3c9147 100644 --- a/docs/email-security/user-reports.md +++ b/docs/email-security/user-reports.md @@ -146,6 +146,9 @@ row still reads "previously resolved `benign` by `system:automated-sender`" rather than erasing the very thing being disputed — and `reopened_from` names the state it came out of. +To remediate a group after reopening, start a new attempt and preview. The +previous resolution's confirmation cannot adopt its completed recipient selection. + Reopening a report that is already `open` or `triaging` succeeds and reports `already_open`, so two analysts clicking at once is not an error. An unknown report id **is** an error rather than a silent success, because this names one From 8a37d2afef007aae7db40bdbf564dde4d63ce4d3 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Thu, 1 Oct 2026 23:23:59 +0000 Subject: [PATCH 6/6] Document null disposition and bulk retry outcomes Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/email-security/messages.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/email-security/messages.md b/docs/email-security/messages.md index 882d55ed0..6c1b2a705 100644 --- a/docs/email-security/messages.md +++ b/docs/email-security/messages.md @@ -493,8 +493,8 @@ Disposition records the security team's decision independently of the engine verdict: `malicious`, `spam`, `graymail`, `benign`, or `simulation`. Setting it preserves the verdict and runs no automations. The decision includes a note of up to 1024 characters, authenticated actor, source, and server timestamp. Message -lists expose `disposition`; message detail includes `disposition_info` and -`disposition_seq`. +lists expose `disposition`, which is `null` when no decision is set or it was +cleared; message detail includes `disposition_info` and `disposition_seq`. ```bash limacharlie mailsec message disposition --disposition spam --note "Reviewed" --oid $OID @@ -504,8 +504,11 @@ limacharlie mailsec message bulk-disposition --msg-uuids --msg-uuids ``` These writes require `mailsec.set`. Bulk input is limited to 500 unique IDs and -returns a result for every message, including individual failures. The CLI exits -with a failure status when any member fails. The `none` list filter selects +returns one ordered result per message, including individual failures. A missing +message fails alone without affecting the others. An error starting with `not +confirmed, retry the same decision` means the outcome is unknown; repeating the same +request is safe because an identical decision is a no-op. The CLI exits with a +failure status when any member fails. The `none` list filter selects messages with no current disposition, including cleared decisions. A benign decision removes this message's credited sender-history flag; malicious