From 9d1302ef0a1bdbe88ffb927d12822e05583e8823 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:18:33 +0200 Subject: [PATCH] feat: skip server-managed vacation script in Sieve script handling --- stores/__tests__/filter-store.test.ts | 34 +++++++++++++++++++++++++++ stores/filter-store.ts | 12 +++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/stores/__tests__/filter-store.test.ts b/stores/__tests__/filter-store.test.ts index 3c27ea4b..a16fdac1 100644 --- a/stores/__tests__/filter-store.test.ts +++ b/stores/__tests__/filter-store.test.ts @@ -339,5 +339,39 @@ describe('filter-store', () => { await useFilterStore.getState().fetchFilters(mockClient as unknown as IJMAPClient); expect(useFilterStore.getState().rawScript).toBe(script); }); + + it('should skip the server-managed vacation script', async () => { + const { generateScript } = await import('@/lib/sieve/generator'); + const rules = [makeRule({ name: 'MyFilter' })]; + const script = generateScript(rules); + + const mockClient = { + getSieveCapabilities: () => null, + getSieveScripts: async () => [ + { id: 'vac-1', name: 'vacation', blobId: 'bv', isActive: true }, + { id: 's1', name: 'filters', blobId: 'b1', isActive: false }, + ], + getSieveScriptContent: async (blobId: string) => { + if (blobId === 'b1') return script; + return 'require "vacation"; vacation "I am away";'; + }, + }; + await useFilterStore.getState().fetchFilters(mockClient as unknown as IJMAPClient); + // Should pick the 'filters' script, not the 'vacation' one + expect(useFilterStore.getState().activeScriptId).toBe('s1'); + expect(useFilterStore.getState().rules[0].name).toBe('MyFilter'); + }); + + it('should handle only vacation script present (no filter scripts)', async () => { + const mockClient = { + getSieveCapabilities: () => null, + getSieveScripts: async () => [ + { id: 'vac-1', name: 'vacation', blobId: 'bv', isActive: true }, + ], + }; + await useFilterStore.getState().fetchFilters(mockClient as unknown as IJMAPClient); + expect(useFilterStore.getState().activeScriptId).toBeNull(); + expect(useFilterStore.getState().rules).toEqual([]); + }); }); }); diff --git a/stores/filter-store.ts b/stores/filter-store.ts index 455998e4..147e1ff4 100644 --- a/stores/filter-store.ts +++ b/stores/filter-store.ts @@ -52,8 +52,12 @@ export const useFilterStore = create()((set, get) => ({ const capabilities = client.getSieveCapabilities(); set({ sieveCapabilities: capabilities }); - const scripts = await client.getSieveScripts(); - debug.log('Sieve scripts fetched:', scripts.length); + const allScripts = await client.getSieveScripts(); + debug.log('Sieve scripts fetched:', allScripts.length); + + // Skip the server-managed 'vacation' script (RFC 9661 §4) — it can only + // be modified via VacationResponse/set, not SieveScript/set. + const scripts = allScripts.filter(s => s.name !== 'vacation'); const activeScript = scripts.find(s => s.isActive) || scripts[0]; if (!activeScript) { @@ -163,7 +167,9 @@ export const useFilterStore = create()((set, get) => ({ // Always re-fetch scripts from the server to get the current state // after Stalwart may have rewritten the active script. - const scripts = await client.getSieveScripts(); + const allScripts = await client.getSieveScripts(); + // Skip the server-managed 'vacation' script (RFC 9661 §4) + const scripts = allScripts.filter(s => s.name !== 'vacation'); const activeScript = scripts.find(s => s.isActive) || scripts[0]; let rules = previousRules;