feat: Hey-style email screening (unify Blocked/Junk into Screened Email Address)#600
Conversation
Combine the "Blocked Email Address" and "Junk Email Address" doctypes into
a single "Screened Email Address" doctype with an `action` field:
- Reject: discard incoming mail from the sender silently
- Spam: file incoming mail into the Spam (Junk) folder
A sender has at most one screening rule (uniqueness on account_id + email),
and Reject supersedes Spam so the two can never both fire for one sender.
Backend:
- New doctype + unified `get_screened_email_addresses`
- Single `update_sieve_script_for_screened_emails` that rebuilds both sieve
blocks and cleans up the legacy "Blocked Emails"/"Junk Senders" blocks
- New API: get_screened_addresses, screen_email_address(es),
unscreen_email_addresses (with Reject-supersedes-Spam upsert)
Frontend:
- Store resource screenedAddresses ([{email, action}])
- Block/junk flows route through the action param
- "Block List" settings tab becomes "Screened Senders" with an action selector
Migration:
- Patch migrates Blocked -> Reject and Junk -> Spam, then drops the old
doctypes and tables
- Guard legacy patches that referenced the removed doctypes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FormControl forces `w-full` on type="select", so as a direct flex sibling it claimed the whole row and squeezed the email input. Wrap it in a fixed-width container so its w-full fills that instead of the row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The controller only regenerated the sieve on insert/delete, so editing the
action (e.g. Spam <-> Reject) in Desk left the sieve stale. Use on_update
(fires on insert and save) gated on has_value_changed("action") so it
regenerates exactly when the sender moves between sieve blocks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A (backend). Add a per-account "Enable Screening" setting that, when on, routes mail from senders not on the Accepted list into a "Screening" folder; only accepted senders (and the account's own identities) reach the inbox. - Screened Email Address: add "Accepted" action - Account Settings: enable_screening flag; regenerate sieve on change; surface it in get_user_info - sieve: auto-create the Screening mailbox and emit a screening gate after the Reject/Spam blocks and above the mailbox automation rules - screen_email_addresses: add `override` so explicit actions overwrite while automated auto-junk does not clobber a manual decision Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B (frontend) on top of the screening engine: - Account Settings: "Screen New Senders" toggle bound to enable_screening; on save, sync the live account and reload mailboxes so the Screening folder appears - Sidebar: surface the Screening folder with a scan-eye icon, grouped with the default mailboxes - MailActions: in the Screening folder, "Accept Sender" (let future mail in, move this message to Inbox) and "Reject Sender" (discard future, move to Trash); hide generic Block/Unblock there - store: screeningMailboxId selector for the roleless Screening folder Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase C. When screening is enabled, allowlist the To/Cc/Bcc of mail you send (action=Accepted, override=False) so replies to threads you start reach the inbox instead of Screening. Wired into both SPA send paths (create_mail, update_draft_mail) beside contact creation. Non-overriding so it never un-rejects a blocked sender; failures are logged and swallowed so they never block sending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A dedicated Screener page for the Screening folder, grouped by unique sender with the latest message as the summary row. Backend (api/mail.py): - get_screening_senders: group Screening-folder mail by sender (latest + count + unread) - get_screening_sender_mails: all of a sender's screened messages - allow_screening_senders: accept + move their mail to Inbox - screen_out_senders: mark Spam + move their mail to Junk (list-based, so per-row and bulk share one path / one sieve regen) Frontend: - ScreenerView with banner (Allow all / Screen all out), per-sender Allow / Screen out, and inline expansion listing all of a sender's messages - /screener route; sidebar routes the Screening folder here as "Screener" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the Screener page (HEY-style first-contact gate) as a quiet, full-width list: stacked sender / subject / body rows with the time and persistent Allow / Block actions parked in each row's corner. Reuses the mail-list-item font styles and spacing, an "All Mails"-style count bar, and a leave animation as rows are screened. Also pin the Screener to its own nameless group at the top of the sidebar with the lucide "eye" icon, showing the pending count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the theme-cycle shortcut and cycleTheme() out of MailboxView into the useTheme composable, and register the Cmd/Ctrl+Shift+L keydown globally in App.vue so it works on every page, not just the mailbox. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the screener row typography/spacing with the mail list item, and stop the sidebar count from hiding on hover for items without a row menu (the Screener), so its pending count stays visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the mobile menu button to the Screener header so the sidebar can be opened on mobile (matching the mailbox). Commonify the Block/Allow text actions into a .screener-action class (via @apply) with an expanded, layout-neutral hit area. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the thread message header, show the sender email without <…> brackets and color the details chevron the same as the email (text-ink-gray-5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking a Screener row now opens that sender's messages in a read-only reuse of the thread template — split beside the list when the reading pane is on, full-width otherwise. A `readonly` mode on MailThread hides the thread toolbar, per-message actions, the block banner and the reply/forward bar, disables the reply/forward shortcuts, and never marks mail as read. The preview header carries the subject plus Block / Allow and a back button; the empty pane reuses the mailbox's "no selection" screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-sender fetch relied on the JMAP `from` query filter, which Stalwart implements as a tokenized text match — so it could return other senders whose From header shared tokens, showing unrelated mail in the preview. Keep only exact-address matches after serializing (as the sender list already does when grouping). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the separate screeningMailboxId computed with a `screening` entry on mailboxIds (populated from the roleless "Screening" folder), and point all usages at store.mailboxIds.screening. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Poll the Screening folder's count every 30s and only refetch the sender list when it changes (the mailbox's cheap-count-then-reload approach). Add ↑/↓ (or k/j) to step through senders and Esc to close while a preview is open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ender Allow/Block now removes the row immediately (no transition, no global disable, no per-action toast — the row leaving is the confirmation) and only surfaces failures, resyncing the list. Acting on the sender open in the detail view advances to the next sender down instead of closing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The message header is click-to-collapse; clicking selected its text and scroll-revealed the truncated sender email. Add select-none so the ellipsis survives a click (matches the mail-list rows). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
undoAction is module-level and the toast is global, so a lingering Undo could fire into another view. Reset it on unmount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move SCREENING_MAILBOX_NAME into constants and add getMailboxName() (returns "Screener" for the Screening folder) and an eye default in getIcon(). Use them across the sidebar, the move/add destination menus and their toasts, so the folder reads as "Screener" with the eye icon everywhere — and it stays a valid Move To / Add To destination. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render a single centered empty/loading screen (no split) when there are no senders — matching the mailbox's height/centering — with a spinner while loading and a thread skeleton in the preview pane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fast arrow-key navigation fires several get_screening_sender_mails fetches at once, and the resource flips `loading` off on the first reply that lands — so an out-of-order reply could mount the preview on the previous sender (which the thread then appended the next one onto). Drive the preview from a token-guarded ref instead of the resource's data: only the most recent fetch is applied. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the content in a bounded flex column so the ListView fills the settings panel and scrolls within itself (no empty space below), and give the columns fr widths so they share the row instead of overflowing into a second scrollbar. Also relabel the actions to Block / Move to Junk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New accounts now default to Hey-style screening (Account Settings.enable_screening). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a full-width info bar above the inbox list, mirroring the trash/junk bar: while screening is on and the Screening folder has unread threads, it links to the Screener. Also reword the Screener copy from "first-time" to "new" senders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This reverts commit 7355c18. Drop the local-domain screening bypass — senders from domains hosted on this server are no longer auto-trusted. The screening gate again trusts only the account's own identity and explicitly accepted senders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rules Reorder the screening sieve to Reject -> Spam -> Mailbox automation -> Screening. The gate is a `not <trusted>` catch-all, so it now sits below the folder rules: mail you route to a folder lands there instead of the Screener, and only unrecognised mail nothing else matched falls into Screening. The gate is re-pinned to the bottom whenever a mailbox block is appended. Also stop the "sender is blocked" alert from being dismissable (the prop was misspelled `dismissable`, so frappe-ui defaulted it to dismissible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lContent Closes the remaining frappe#452 gap: as well as <img src>, neutralize remote url(...) in inline styles and <style> blocks (background images, fonts), so no external request leaks the read. http(s) and protocol-relative // both count; cid:/data: still load. Move the asset blocking, detection (analyzeRemoteAssets), and quote-collapse off regex onto DOMParser — robust against markup quirks. The quote-collapse in particular was buggy: its non-greedy match stopped at the first </div>, so a quote with nested divs collapsed the wrong region; it now wraps the whole .gmail_quote element. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confidence Score: 3/5The migration path and sieve rebuild path both have error-handling gaps that could silently drop folder automation rules or leave the screening gate misconfigured with no diagnostic trace. The
|
| Filename | Overview |
|---|---|
| mail/api/sieve.py | Major refactor replacing per-function sieve builders with a unified build_automation_sieve that regenerates the entire script from persistent backups; adds Reject/Spam/Screening block generation and a pause_automation_sieve_build context manager. The backfill_mailbox_automation_rules migration helper silently swallows all exceptions with no logging, risking undetected data loss during migration. |
| mail/api/mail.py | New Screener endpoints (get_screening_senders, allow_screening_senders, screen_out_senders, move_screening_mails_to_inbox), unified screening logic in _screen_email_addresses, and auto-accept-on-send. JMAP N+1 pattern in allow_screening_senders/screen_out_senders (O(N) queries per sender, already flagged for tokenized-filter issues in prior threads). |
| mail/client/doctype/screened_email_address/screened_email_address.py | New unified doctype merging Blocked/Junk email address handling; correct permission scoping, uniqueness enforcement at DB level, and sieve rebuild hooks. get_screened_email_addresses lacks a query limit which may be a concern for large screening lists. |
| mail/patches/migrate_to_screened_email_address.py | Migration patch merging Blocked (→ Reject) and Junk (→ Spam) email addresses into Screened Email Address; uses raw DDL with f-string table name for the DROP TABLE step, which is a style convention concern (values are hardcoded, no injection risk). |
| mail/patches/backfill_mailbox_automation_rules.py | Thin patch that enqueues backfill_mailbox_automation_rules as a background job; the actual backfill logic in sieve.py silently swallows all JMAP/DB errors with no logging. |
| mail/client/doctype/account_settings/account_settings.py | Adds on_update hook to regenerate the automation sieve when enable_screening is toggled; correctly guarded for in_migrate and has_value_changed. |
| mail/client/doctype/mailbox_settings/mailbox_settings.py | Adds on_update hook for automation rule changes with pause_automation_sieve_build flag guard; adds get_mailbox_automation_rules and automation_rules_to_settings helpers. Logic is clean and idempotent. |
| frontend/src/pages/ScreenerView.vue | New Screener page with sender-grouped list, per-sender Allow/Block, read-only thread preview with race-safe token-based loading, keyboard navigation, polling, and Clear All. Logic is well-structured. |
| frontend/src/components/EmailContent.vue | Adds remote image blocking banner and DOM-based quote collapsing (replacing buggy regex); image blocking/reveal flow is correctly tied to blockImages prop and trust emit. |
| frontend/src/utils/useThreadActions.ts | Extends mark-as-junk flow to pass screen_action in the same JMAP call; undo correctly reverses the screening rule; getMailboxName replaces raw _name access for Screener folder label. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming mail] --> B{Reject block\nsieve match?}
B -- yes --> C[discard / stop]
B -- no --> D{Mailbox automation\nrule match?}
D -- yes --> E[fileinto target folder]
D -- no --> F{Spam block\nsieve match?}
F -- yes --> G[addflag $junk\nfileinto Junk]
F -- no --> H{Screening enabled?}
H -- no --> I[Inbox]
H -- yes --> J{From in\naccepted + own-identity list?}
J -- yes --> I
J -- no --> K[fileinto Screener]
K --> L[Screener UI:\nAllow / Block]
L -- Allow --> M[accept sender\nmove to Inbox]
L -- Block --> N[mark Spam\nmove to Junk]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Incoming mail] --> B{Reject block\nsieve match?}
B -- yes --> C[discard / stop]
B -- no --> D{Mailbox automation\nrule match?}
D -- yes --> E[fileinto target folder]
D -- no --> F{Spam block\nsieve match?}
F -- yes --> G[addflag $junk\nfileinto Junk]
F -- no --> H{Screening enabled?}
H -- no --> I[Inbox]
H -- yes --> J{From in\naccepted + own-identity list?}
J -- yes --> I
J -- no --> K[fileinto Screener]
K --> L[Screener UI:\nAllow / Block]
L -- Allow --> M[accept sender\nmove to Inbox]
L -- Block --> N[mark Spam\nmove to Junk]
Reviews (10): Last reviewed commit: "refactor(mailbox): build the automation ..." | Re-trigger Greptile
Resolve conflicts and conform the screening feature to develop's account → account_id API refactor: whitelisted endpoints take account_id and resolve via get_session_account; jmap helpers are called with *parse_account(account); the Screened Email Address doctype is keyed on the shared account_id (per-user account/user fields dropped) with account-scoped permissions. Drop develop's now-superseded Blocked/Junk functions; frontend screening calls pass account_id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
ToDo:
|
Settings > Screened Senders only offered Block and Move to Junk. Add an Accept option (action "Accepted") so a sender can be allowlisted into the inbox directly from the list, matching the per-message "Accept Sender" action. The backend and types already supported "Accepted". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The banner kept showing a Hide/Load Images toggle after the reader opted in. Once images are loaded there's nothing left to act on, so dismiss the banner instead of flipping the button to "Hide Images". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mages" Reorder the two controls in Settings > Account so "Block Remote Images" comes first and "When Marking as Junk" follows it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion script get_automation_script_name receives a full `user:account_id` handle but passed it straight to create_automation_script, which reconstructs the session handle itself via get_session_account — yielding `user:user:account_id` and a parse_account failure. This surfaced when enabling the screener for an account whose automation script didn't exist yet. Pass the bare account_id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spam Senders previously sat at the top alongside Reject, above the mailbox rules, so a spam-filed sender was junked before any explicit folder rule could claim the mail. Move Spam below the mailbox rules (still above the Screening gate) so precedence is Reject → Mailbox → Spam → Screening. Generalize the move-to-bottom helper to push both Spam and Screening back below mailbox blocks when a mailbox rule is appended, preserving the order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Add an Automation section to Mailbox Settings (emails_from, subject_contains, match_if, mark_as_read, add_star) so folder automation rules have a durable backup independent of the frappe_mail_automation Sieve script. Add get_mailbox_automation_rules() to read them back as a rule dict and automation_rules_to_settings() to flatten a rule dict onto the fields. This makes Mailbox Settings the source of truth: the Sieve script becomes a derived artifact that can be regenerated from here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive the frappe_mail_automation script from the persisted Mailbox Settings rules instead of round-tripping them through the script text: - create/update_mailbox store the rules on Mailbox Settings first, then update_sieve_script_for_mailbox reads them back from there to build the block (children included, so a parent rename refreshes their paths). - get_mailboxes now returns each mailbox's automation_rules so the UI can read the backup directly. - Add rebuild_automation_script() (+ whitelisted *_for_account) to fully regenerate the script from Mailbox Settings + Screened Email Address. create_automation_script auto-rebuilds on creation, so a script deleted by a third-party client is restored the next time the app recreates it. - Add backfill_mailbox_automation_rules() and a patch that enqueues it, to capture existing in-script rules into the backup before any rebuild can drop them (JMAP isn't reachable during migrate, so it runs as a job). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utton - FolderModal now reads automation rules from mailbox.automation_rules (the Mailbox Settings backup served by get_mailboxes) instead of regex-parsing the Sieve script, so rules show even if a third-party client deleted it. - Add a "Rebuild Automation" button to the Sieve Scripts settings that regenerates the automation script from the saved rules. - Add the AutomationRules type and automation_rules to MailboxData. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mailbox list is held in a per-process TTL cache that service.mailboxes reads. add_mailbox/update_mailbox invalidated it with the full user:account_id handle, but the cache is keyed by the bare account_id, so the call was a no-op — and across workers the cache can outlive a mailbox's creation entirely. A stale negative for "Screener" made get_screening_folder_path try to recreate it, which JMAP rejects with "already exists", breaking the manual automation rebuild (and any screening-gate generation). - get_screening_folder_path now refreshes the in-process cache before deciding to create, so it never recreates an existing mailbox. - add_mailbox/update_mailbox invalidate with the bare account_id (real key). - delete_mailboxes now invalidates too, so later lookups don't see a deleted mailbox. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sieve Introduce a single entry point, build_automation_sieve(account, activate), that ensures the frappe_mail_automation script exists, regenerates all four sections from their backups (Reject -> Mailbox -> Spam -> Screening), and optionally activates it. - Add pause_automation_sieve_build() context manager and maybe_build_automation_sieve() so document hooks can rebuild once after a bulk write instead of after every write. - Drop the create -> rebuild re-entrancy: get_automation_script_name() now only ensures an empty script exists; create_automation_script() delegates to the unified builder. Removes the in_automation_rebuild flag. - Fold the per-mailbox and per-screened-email incremental builders into the full rebuild; keep update_sieve_script_for_mailbox/_screened_emails as thin delegating shims until their callers are migrated. - Remove now-dead helpers (_move_fallback_blocks_to_bottom, _extract_sieve_block, get_child_mailbox_names, _mailbox_automation_rules_by_name). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sieve Route every Screened Email Address / screening change through the unified builder instead of the old screened-emails-only path. - Screened Email Address on_update/after_delete now call maybe_build_automation_sieve(), so a paused bulk write rebuilds once. - Account Settings enable_screening toggle and the bulk screen/unscreen endpoints (which bypass document hooks) call build_automation_sieve directly. - Remove the update_sieve_script_for_screened_emails shim now that nothing calls it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route Mailbox Settings automation changes through the unified builder. - Add Mailbox Settings on_update: a save through the document lifecycle that changes an automation field (emails_from, subject_contains, match_if, mark_as_read, add_star) rebuilds the script via maybe_build_automation_sieve. Skipped during migrate and when a bulk write paused builds. - create/update/delete_mailbox now pause the per-write rebuild around their structural + Mailbox Settings writes and call build_automation_sieve once at the end (after a rename lands, so folder paths regenerate correctly). - Remove the update_sieve_script_for_mailbox shim now that nothing calls it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…il Address (backport frappe#600 to v0) Backport of PR frappe#600 (frappe/mail, merged into develop) onto v0. Merges the legacy "Blocked Email Address" and "Junk Email Address" doctypes into a single account-scoped "Screened Email Address" doctype (Reject / Spam / Accepted actions), adds the Hey-style screening flow (Screener view + screening endpoints), and generates the frappe_mail_automation sieve through the unified build_automation_sieve layer (create-if-missing, build all four sections — Reject -> Mailbox -> Spam -> Screening — and optionally activate). A migration converts existing Blocked -> Reject and Junk -> Spam rules and drops the old doctypes. v0 adaptations during the backport: - mail/api/mail.py: kept v0's get_mail_config (develop renamed it to get_config) and v0's import layout while adding the screening imports. - mail/hooks.py: applied only frappe#600's change (drop Blocked/Junk permission and ignore-links entries, add Screened Email Address); did not pull in the unrelated develop-only Calendar Exchange permission entry that v0 doesn't register. - frontend/src/types/doctypes.ts: kept v0's generated types; frappe#600's only diff here was unrelated MailSettings log-field regeneration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: Hey-style email screening; unify Blocked/Junk into Screened Email Address (backport #600 to v0)
Overview
Adds Hey-style screening for incoming mail, built on a unified screening doctype.
First, the two near-identical doctypes Blocked Email Address and Junk Email Address are merged into a single Screened Email Address with an
actionfield. That same doctype then powers an opt-in screening flow: unknown senders are quarantined into a Screening folder, and only senders you accept reach the inbox — managed from a dedicated Screener page.A sender has at most one screening rule (uniqueness on
account_id+email), with oneaction:AcceptedSpamRejectEverything is enforced server-side via the automation sieve script — the same mechanism the old block/junk lists used.
1. Doctype unification + migration
(account_id, email), so a shared account's list is consistent for everyone with access.update_sieve_script_for_screened_emailsrebuilds the Reject/Spam/Screening blocks and cleans up the legacyBlocked Emails/Junk Sendersblocks.migrate_to_screened_email_address: Blocked →Reject, Junk →Spam(Reject wins on collision), then drops the old doctypes and tables. Legacy patches guarded so fresh installs don't reference the removed doctypes.screen_email_addresses(..., override): explicit user actions overwrite an existing rule; the automated auto-junk flow passesoverride=Falseso it never clobbers a manual decision.2. Screening engine (opt-in, per account)
not <trusted>catch-all that runs after Reject, Spam, and your folder automation rules, so anything you've routed or accepted is handled before it:Screeningmailbox (not a standard JMAP role) is auto-created on first enable.3. Screening folder UX
4. Auto-accept on send
Accepted, non-overriding) so replies to threads you start reach your inbox instead of Screening.5. Screener page
A dedicated view for the Screening folder, grouped by unique sender (latest message as the summary row):
get_screening_senders,get_screening_sender_mails,allow_screening_senders,screen_out_senders.6. Mark Junk / Not Junk ↔ screening
Accepted). The screening change rides on the same request as the mail move (set_mails_spam_status(..., screen_action)), and the reversal rides on the same request as the undo's mailbox restore (set_mails_mailboxes(..., screen_action)) — so an immediate Cmd+Z flips the rule instead of leaving it stale or deleting it.$junkas well as filed into Junk, so it's genuinely marked junk — matching what marking a mail as junk does — not just placed in the folder.7. Block remote images (#452)
<img src>and CSSurl(...)in inline styles and<style>blocks (background images, fonts), forhttp(s)and protocol-relative//; inlinecid:(attachments) anddata:images still load.<div>s.8. Inbox banner for unscreened mail
9. Turning screening off
/screenerrenders nothing and redirects to the inbox.move_screening_mails_to_inbox).Notes
Acceptedaction.account_id(matching the old Blocked behavior). On a shared account, Spam/Accepted rules are shared across users — same as block rules previously.pre-commit(ruff + prettier/eslint) clean and the frontend builds. The JMAP round-trip (sieve routing, sender grouping, Allow/Block moves, image blocking) should be verified on a live Stalwart account.🤖 Generated with Claude Code