From 048d64b5c8c23b7c0dfcc7369571264c077d9f4c Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 7 Sep 2026 07:32:18 +0100 Subject: [PATCH 01/16] Add visual JMAP Sieve mail rules Adds the managed rule model, browser editor, RFC 9661 persistence path, and test coverage. Existing external scripts remain preserved unless the user explicitly confirms activation. --- specs/004-mail-rules/spec.md | 44 + src/App.vue | 11 +- src/components/AccountAvatarMenu.vue | 14 +- src/components/MailRulesDialog.vue | 906 ++++++++++++++++++ src/constants/states.ts | 1 + src/db/protocol.ts | 1 + src/db/repository.ts | 4 + src/sieve/rules.ts | 469 +++++++++ src/stores/rules-store.ts | 171 ++++ src/sync/backends/jmap/backend.ts | Bin 90915 -> 90059 bytes src/sync/backends/jmap/outbox/index.ts | 19 +- src/sync/backends/jmap/sieve.ts | 345 +++++++ src/sync/backends/jmap/transport.ts | 2 + src/sync/sync-host.ts | 6 + tests/e2e/mail-rules.spec.js | 155 +++ tests/unit/components/app-layout.test.ts | 4 + .../unit/components/mail-rules-dialog.test.ts | 159 +++ tests/unit/sieve/rules.test.ts | 175 ++++ tests/unit/stores/rules-store.test.ts | 148 +++ tests/unit/sync/_mock-transport.ts | 24 + tests/unit/sync/jmap-outbox-sieve.test.ts | 90 ++ tests/unit/sync/jmap-sieve.test.ts | 242 +++++ 22 files changed, 2986 insertions(+), 4 deletions(-) create mode 100644 specs/004-mail-rules/spec.md create mode 100644 src/components/MailRulesDialog.vue create mode 100644 src/sieve/rules.ts create mode 100644 src/stores/rules-store.ts create mode 100644 src/sync/backends/jmap/sieve.ts create mode 100644 tests/e2e/mail-rules.spec.js create mode 100644 tests/unit/components/mail-rules-dialog.test.ts create mode 100644 tests/unit/sieve/rules.test.ts create mode 100644 tests/unit/stores/rules-store.test.ts create mode 100644 tests/unit/sync/jmap-outbox-sieve.test.ts create mode 100644 tests/unit/sync/jmap-sieve.test.ts diff --git a/specs/004-mail-rules/spec.md b/specs/004-mail-rules/spec.md new file mode 100644 index 00000000..ab336af0 --- /dev/null +++ b/specs/004-mail-rules/spec.md @@ -0,0 +1,44 @@ +# Visual server-side mail rules + +**Status**: Prototype proposed upstream +**Proposal**: https://ideas.tb.pro/p/visual-server-side-mail-rules-using-jmap-sieve + +## Goal + +Stormbox shall let a signed-in user build ordered mail rules visually and run them on the mail server through JMAP for Sieve (RFC 9661). The feature remains fully browser-owned and does not add a Stormbox application backend. + +## Requirements + +| ID | Requirement | +|---|---| +| MR-1 | When the account advertises `urn:ietf:params:jmap:sieve`, the account menu shall offer a Mail Rules editor. When the capability is absent, the editor shall explain that server-side rules are unavailable. | +| MR-2 | The editor shall support ordered, enabled/disabled rules; all/any condition matching; From, To, To/Cc, Subject, and custom-header conditions; and exact, contains, or wildcard matching. | +| MR-3 | The editor shall support move, mark read, star, forward a copy, discard, and stop-processing actions, exposing only actions supported by the account's advertised Sieve extensions. | +| MR-4 | Stormbox shall store the visual rule document as versioned metadata in a clearly marked managed Sieve script. Generated Sieve is a build artifact rather than the source model. | +| MR-5 | Every save shall use the durable mutation outbox, upload the generated script, call `SieveScript/validate`, and activate it only after validation succeeds. | +| MR-6 | A save shall use the last observed `SieveScript` state and reject a concurrent server change instead of overwriting it. | +| MR-7 | Stormbox shall not modify or delete a script it does not own. If a foreign script is active, activation shall require explicit confirmation and shall preserve the foreign script on the server. | +| MR-8 | Move actions shall use the RFC 9042 `:mailboxid` extension when supported, retaining a readable hierarchy path as the fallback mailbox name. | +| MR-9 | Invalid rule data, unsupported actions, server validation failures, and server conflicts shall remain visible and recoverable in the editor. Controls shall be disabled while a save is in flight. | +| MR-10 | The editor shall be keyboard accessible, trap focus while open, and confirm before discarding unsaved changes. | + +## Initial scope + +- Server-side filtering of newly delivered mail. +- One managed visual-rule script per account. +- A deliberately limited, typed rules model and purpose-built Sieve emitter. +- Existing arbitrary Sieve scripts remain opaque and preserved. + +## Non-goals + +- Importing arbitrary Sieve into the visual model. +- A raw Sieve editor. +- Retroactively applying rules to existing messages. +- Shared-account rule management. +- A Cloudflare Worker rule engine; the deployment bridge only adapts browser CORS and WebSocket authentication. + +## Verification + +- Unit tests cover normalization, escaping, capability checks, metadata round-tripping, conflict handling, server validation, and outbox integration. +- Component tests cover loading, editing, takeover confirmation, save locking, and discard confirmation. +- Local-stack Playwright coverage asserts the visible editor result, durable mutation completion, and the active script directly through JMAP in Chromium and Firefox. diff --git a/src/App.vue b/src/App.vue index b87e63b7..29deb746 100644 --- a/src/App.vue +++ b/src/App.vue @@ -45,6 +45,7 @@ import AppDrawer from './components/AppDrawer.vue'; import TopNavMenu from './components/TopNavMenu.vue'; import AccountAvatarMenu from './components/AccountAvatarMenu.vue'; import WelcomeModal from './components/WelcomeModal.vue'; +import MailRulesDialog from './components/MailRulesDialog.vue'; import SettingsDialog from './components/settings/SettingsDialog.vue'; import SettingsGearButton from './components/settings/SettingsGearButton.vue'; // Staff-only Kanban feature (src/features/kanban): the settings dialog's @@ -153,6 +154,7 @@ const messageListWidth = ref(DEFAULT_COLUMN_WIDTHS.messageList); const folderListHidden = ref(false); const showWelcomeModal = ref(false); const showSettingsDialog = ref(false); +const showMailRules = ref(false); // With 'system' the OS decides, so a manual light/dark button would fight it. const showThemeToggle = computed(() => theme.value !== 'system'); // Modal dialogs own the keyboard: a single-letter mail shortcut must not @@ -160,7 +162,8 @@ const showThemeToggle = computed(() => theme.value !== 'system'); const shortcutsEnabled = computed(() => authStore.status === AUTH_STATE.CONNECTED && !showWelcomeModal.value - && !showSettingsDialog.value, + && !showSettingsDialog.value + && !showMailRules.value, ); const windowWidth = ref(typeof window === 'undefined' ? COMPACT_READING_WIDTH : window.innerWidth); const wantsMessageDetailView = computed(() => @@ -771,7 +774,10 @@ function clamp(value: number, min: number, max: number) { @toggle-theme="toggleTheme" @open-settings="showSettingsDialog = true" /> - + @@ -886,6 +892,7 @@ function clamp(value: number, min: number, max: number) { singular-item-label="message" :total="mailStore.bulkOperation.total" /> + import { computed, ref } from 'vue'; import { onClickOutside } from '@vueuse/core'; -import { LogOut, RotateCcw, Settings } from '@lucide/vue'; +import { + ListFilter, LogOut, RotateCcw, Settings, +} from '@lucide/vue'; import { useAuthStore } from '../stores/auth-store'; import { ACCOUNTS_URL } from '../defines'; @@ -9,6 +11,7 @@ import { senderAvatarStyle, senderInitials } from '../utils/sender-avatar'; const authStore = useAuthStore(); const emit = defineEmits<{ + (event: 'show-mail-rules'): void; (event: 'show-welcome-modal'): void; }>(); @@ -34,6 +37,11 @@ function onShowWelcomeModal() { if (detailsEl.value) detailsEl.value.open = false; emit('show-welcome-modal'); } + +function onShowMailRules() { + if (detailsEl.value) detailsEl.value.open = false; + emit('show-mail-rules'); +}