Skip to content
This repository was archived by the owner on Jun 25, 2026. It is now read-only.

feat: Hey-style email screening (unify Blocked/Junk into Screened Email Address)#600

Merged
s-aga-r merged 51 commits into
frappe:developfrom
s-aga-r:feat/screened-email-address
Jun 25, 2026
Merged

feat: Hey-style email screening (unify Blocked/Junk into Screened Email Address)#600
s-aga-r merged 51 commits into
frappe:developfrom
s-aga-r:feat/screened-email-address

Conversation

@s-aga-r

@s-aga-r s-aga-r commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

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 action field. 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 one action:

Action Effect on incoming mail
Accepted Delivered to the inbox
Spam Filed into the Spam (Junk) folder
Reject Discarded silently

Everything is enforced server-side via the automation sieve script — the same mechanism the old block/junk lists used.

1. Doctype unification + migration

  • New Screened Email Address doctype (replaces Blocked + Junk); uniqueness on (account_id, email), so a shared account's list is consistent for everyone with access.
  • Unified sieve generation: one update_sieve_script_for_screened_emails rebuilds the Reject/Spam/Screening blocks and cleans up the legacy Blocked Emails / Junk Senders blocks.
  • Patch 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 passes override=False so it never clobbers a manual decision.

2. Screening engine (opt-in, per account)

  • Account Settings → Enable Screening (default off, so existing/shared accounts are untouched).
  • When on, the automation sieve gains a screening gate that is the last block in the script — a 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:
    # Screening   (last block — the fallback)
    if not address :is "from" [ <accepted senders> + <your own identities> ] {
      fileinto "Screening";
      stop;
    }
    
    Net order: Reject → discard · Spam → Junk · folder rules → their folders · unknown → Screening · accepted/own → inbox. (A sender you route to a folder skips the Screener; only mail nothing else matched, from an untrusted sender, is held.)
  • The Screening mailbox (not a standard JMAP role) is auto-created on first enable.

3. Screening folder UX

  • "Screen New Senders" toggle in Account Settings.
  • Sidebar surfaces the folder as Screener (scan-eye icon), grouped with the default mailboxes.
  • In-message Accept Sender / Reject Sender actions inside the Screening folder.

4. Auto-accept on send

  • When screening is on, recipients of mail you send are allowlisted (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):

  • Banner with first-time-sender count + Clear All (moves every queued message to the Inbox without blocking or allowing anyone — a safe "empty the queue, I'll triage in my inbox").
  • Per-sender Allow (accept → move their mail to Inbox) and Block (mark Spam → move to Junk).
  • Click a sender to open their mail in a read-only thread preview (split when the reading pane is on); ↑/↓ (or k/j) traverse, Esc closes.
  • JMAP-backed grouping: get_screening_senders, get_screening_sender_mails, allow_screening_senders, screen_out_senders.

6. Mark Junk / Not Junk ↔ screening

  • Marking a thread Junk screens its sender as Spam (their future mail → Junk); marking Not Junk screens them in (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.
  • Screened-out (Spam) mail is flagged $junk as 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)

  • Per-account Block Remote Images setting (default on): mail from senders you haven't accepted has its remote images withheld, so they can't phone home as read-tracking pixels.
  • Covers both <img src> and CSS url(...) in inline styles and <style> blocks (background images, fonts), for http(s) and protocol-relative //; inline cid: (attachments) and data: images still load.
  • Per-message privacy bar: Load / Hide images and Mark sender as trusted (accepts the sender, so their images load now and going forward). Hidden in the Screener preview, where Block/Allow is the trust mechanism.
  • Image blocking/detection run on the DOM (DOMParser) rather than regex; this also fixed a latent bug where the email renderer's quote-collapse mis-matched nested <div>s.

8. Inbox banner for unscreened mail

  • A slim, dismissible banner above the inbox list nudges you to the Screener while the queue is non-empty.

9. Turning screening off

  • The Screener is hidden from the sidebar, and /screener renders nothing and redirects to the inbox.
  • Saving Account Settings with screening turned off prompts to move the Screener's remaining mail to the Inbox (move_screening_mails_to_inbox).

Notes

  • Defaults: screening is opt-in per account; Block Remote Images is on by default. No data migration needed for the new Accepted action.
  • Scoping: the merged doctype scopes all actions to the shared account_id (matching the old Blocked behavior). On a shared account, Spam/Accepted rules are shared across users — same as block rules previously.
  • Testing: 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

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>
@s-aga-r s-aga-r requested a review from krantheman as a code owner June 22, 2026 13:23
s-aga-r and others added 2 commits June 22, 2026 19:14
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>
@s-aga-r s-aga-r marked this pull request as draft June 22, 2026 14:05
s-aga-r and others added 4 commits June 22, 2026 20:13
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>
@s-aga-r s-aga-r changed the title feat: merge Blocked & Junk Email Address into Screened Email Address feat: Hey-style email screening (unify Blocked/Junk into Screened Email Address) Jun 22, 2026
krantheman and others added 20 commits June 23, 2026 14:07
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>
krantheman and others added 3 commits June 24, 2026 17:40
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>
@krantheman krantheman marked this pull request as ready for review June 24, 2026 13:43
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 3/5

The 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 backfill_mailbox_automation_rules helper swallows every JMAP and DB exception with bare except Exception: continue and no logging — if the backfill fails for any user, their existing folder automation rules are silently lost the next time a sieve rebuild runs (since the rebuild reads from Mailbox Settings, which was never populated). This is a data-loss path that fires automatically on migration. The JMAP tokenized-filter bugs in _screening_message_ids already called out in prior review threads also remain: Allow and Block operations can act on messages from unintended senders when From-header tokens overlap.

mail/api/sieve.py (backfill_mailbox_automation_rules — exception handling), mail/api/mail.py (allow_screening_senders, screen_out_senders — JMAP filter correctness and N+1 calls).

Important Files Changed

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]
Loading
%%{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]
Loading

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>
@s-aga-r

s-aga-r commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

ToDo:

  • Allow adding a Screened Sender with action as "Accepted" from Settings > Screened Senders.
  • Ensure the "Screener" folder exists.
  • On the disable Screener, move unscreened emails to the Inbox and unsubscribe/delete the "Screener" folder (?)
  • Hide the remote images banner after clicking the "Load Images" (?)
  • Move "When Marking as Junk" below the "Block Remote Images".
  • Add patch "migrate_to_screened_email_address".
  • Fix sieve rules order:
    1. Reject
    2. Mailbox
    3. Junk
    4. Screener
  • Rebuild automation sieve
  • Unify automation sieve build

Comment thread mail/api/mail.py
Comment thread mail/api/mail.py
s-aga-r and others added 4 commits June 25, 2026 07:04
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>
Comment thread mail/api/sieve.py
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>
Comment thread mail/api/sieve.py Outdated
s-aga-r and others added 8 commits June 25, 2026 08:00
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>
@s-aga-r s-aga-r merged commit b14f3af into frappe:develop Jun 25, 2026
2 of 4 checks passed
s-aga-r added a commit to s-aga-r/mail that referenced this pull request Jun 25, 2026
…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>
s-aga-r added a commit that referenced this pull request Jun 25, 2026
feat: Hey-style email screening; unify Blocked/Junk into Screened Email Address (backport #600 to v0)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants