diff --git a/locales/rn/ar.json b/locales/rn/ar.json index ec7c075d..cf6c0633 100644 --- a/locales/rn/ar.json +++ b/locales/rn/ar.json @@ -670,7 +670,8 @@ "remove_condition": "إزالة الشرط", "sieve_editor": { "line_count": "{count, plural, zero {# سطر} one {سطر واحد} two {سطران} few {# أسطر} many {# سطرًا} other {# سطر}}" - } + }, + "validation_header_name": "أدخل اسم ترويسة لا يحتوي إلا على الأحرف A-Z والأرقام والشرطات والشرطات السفلية، مثل X-Spam-Flag." }, "folders": { "account": "الحساب", diff --git a/locales/rn/ca.json b/locales/rn/ca.json index eea7c99c..a9c1fa22 100644 --- a/locales/rn/ca.json +++ b/locales/rn/ca.json @@ -670,7 +670,8 @@ "remove_condition": "Elimina la condició", "sieve_editor": { "line_count": "{count, plural, one {# línia} other {# línies}}" - } + }, + "validation_header_name": "Introduïu un nom de capçalera que només contingui les lletres A-Z, xifres, guions i guions baixos, com ara X-Spam-Flag." }, "folders": { "account": "Compte", diff --git a/locales/rn/cs.json b/locales/rn/cs.json index 2ed2f8b8..764db36e 100644 --- a/locales/rn/cs.json +++ b/locales/rn/cs.json @@ -670,7 +670,8 @@ "remove_condition": "Odebrat podmínku", "sieve_editor": { "line_count": "{count, plural, one {# řádek} few {# řádky} many {# řádku} other {# řádků}}" - } + }, + "validation_header_name": "Zadejte název hlavičky, který obsahuje jen písmena A–Z, číslice, pomlčky a podtržítka, například X-Spam-Flag." }, "folders": { "account": "Účet", diff --git a/locales/rn/da.json b/locales/rn/da.json index 00376269..7aa65797 100644 --- a/locales/rn/da.json +++ b/locales/rn/da.json @@ -670,7 +670,8 @@ "remove_condition": "Fjern betingelse", "sieve_editor": { "line_count": "{count, plural, one {# linje} other {# linjer}}" - } + }, + "validation_header_name": "Angiv et headernavn, der kun bruger bogstaverne A-Z, tal, bindestreger og understregninger, f.eks. X-Spam-Flag." }, "folders": { "account": "Konto", diff --git a/locales/rn/de.json b/locales/rn/de.json index a47d08dc..9e2fc640 100644 --- a/locales/rn/de.json +++ b/locales/rn/de.json @@ -670,7 +670,8 @@ "remove_condition": "Bedingung entfernen", "sieve_editor": { "line_count": "{count, plural, one {# Zeile} other {# Zeilen}}" - } + }, + "validation_header_name": "Geben Sie einen Header-Namen ein, der nur die Buchstaben A–Z, Ziffern, Bindestriche und Unterstriche enthält, zum Beispiel X-Spam-Flag." }, "folders": { "account": "Konto", diff --git a/locales/rn/en.json b/locales/rn/en.json index 6bcfb6c9..b10fcecb 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -683,7 +683,8 @@ "remove_condition": "Remove condition", "sieve_editor": { "line_count": "{count, plural, one {# line} other {# lines}}" - } + }, + "validation_header_name": "Enter a header name that uses only the letters A-Z, digits, hyphens and underscores, such as X-Spam-Flag." }, "folders": { "account": "Account", diff --git a/locales/rn/es.json b/locales/rn/es.json index ea3a5596..e458f9e4 100644 --- a/locales/rn/es.json +++ b/locales/rn/es.json @@ -670,7 +670,8 @@ "remove_condition": "Eliminar condición", "sieve_editor": { "line_count": "{count, plural, one {# línea} other {# líneas}}" - } + }, + "validation_header_name": "Escribe un nombre de encabezado que solo use las letras A-Z, números, guiones y guiones bajos, por ejemplo X-Spam-Flag." }, "folders": { "account": "Cuenta", diff --git a/locales/rn/fa.json b/locales/rn/fa.json index cfe6e435..1f017036 100644 --- a/locales/rn/fa.json +++ b/locales/rn/fa.json @@ -670,7 +670,8 @@ "remove_condition": "حذف شرط", "sieve_editor": { "line_count": "{count, plural, one {# خط} other {# خط}}" - } + }, + "validation_header_name": "نام هدری وارد کنید که فقط از حروف A-Z، رقم، خط تیره و زیرخط تشکیل شده باشد، مانند X-Spam-Flag." }, "folders": { "account": "حساب", diff --git a/locales/rn/fr.json b/locales/rn/fr.json index e4290ca3..4938bd2a 100644 --- a/locales/rn/fr.json +++ b/locales/rn/fr.json @@ -670,7 +670,8 @@ "remove_condition": "Supprimer la condition", "sieve_editor": { "line_count": "{count, plural, one {# ligne} other {# lignes}}" - } + }, + "validation_header_name": "Saisissez un nom d'en-tête composé uniquement des lettres A à Z, de chiffres, de tirets et de tirets bas, par exemple X-Spam-Flag." }, "folders": { "account": "Compte", diff --git a/locales/rn/he.json b/locales/rn/he.json index 91bbc004..5a3398b9 100644 --- a/locales/rn/he.json +++ b/locales/rn/he.json @@ -670,7 +670,8 @@ "remove_condition": "הסר תנאי", "sieve_editor": { "line_count": "{count, plural, one {שורה אחת} two {שתי שורות} other {# שורות}}" - } + }, + "validation_header_name": "הזן שם כותרת שכולל רק את האותיות A-Z, ספרות, מקפים וקווים תחתונים, למשל X-Spam-Flag." }, "folders": { "account": "חשבון", diff --git a/locales/rn/hu.json b/locales/rn/hu.json index 591e0273..65a24154 100644 --- a/locales/rn/hu.json +++ b/locales/rn/hu.json @@ -670,7 +670,8 @@ "remove_condition": "Feltétel eltávolítása", "sieve_editor": { "line_count": "{count, plural, one {# sor} other {# sor}}" - } + }, + "validation_header_name": "Adj meg egy fejlécnevet, amely csak az A–Z betűket, számjegyeket, kötőjeleket és aláhúzásjeleket tartalmazza, például X-Spam-Flag." }, "folders": { "account": "Fiók", diff --git a/locales/rn/it.json b/locales/rn/it.json index 499e0063..79efd85b 100644 --- a/locales/rn/it.json +++ b/locales/rn/it.json @@ -670,7 +670,8 @@ "remove_condition": "Rimuovi condizione", "sieve_editor": { "line_count": "{count, plural, one {# riga} other {# righe}}" - } + }, + "validation_header_name": "Inserisci un nome di intestazione che usi solo le lettere A-Z, cifre, trattini e trattini bassi, ad esempio X-Spam-Flag." }, "folders": { "account": "Account", diff --git a/locales/rn/ja.json b/locales/rn/ja.json index 6dd22501..d609146d 100644 --- a/locales/rn/ja.json +++ b/locales/rn/ja.json @@ -670,7 +670,8 @@ "remove_condition": "条件を削除", "sieve_editor": { "line_count": "{count, plural, other {#行}}" - } + }, + "validation_header_name": "ヘッダー名には英字(A~Z)、数字、ハイフン、アンダースコアのみを使用してください(例: X-Spam-Flag)。" }, "folders": { "account": "アカウント", diff --git a/locales/rn/ko.json b/locales/rn/ko.json index 1583ba51..9d5b1806 100644 --- a/locales/rn/ko.json +++ b/locales/rn/ko.json @@ -670,7 +670,8 @@ "remove_condition": "조건 제거", "sieve_editor": { "line_count": "{count, plural, other {#줄}}" - } + }, + "validation_header_name": "헤더 이름은 영문자(A-Z), 숫자, 하이픈, 밑줄만 사용해 입력해 주세요(예: X-Spam-Flag)." }, "folders": { "account": "계정", diff --git a/locales/rn/lv.json b/locales/rn/lv.json index 26b46099..f63b110c 100644 --- a/locales/rn/lv.json +++ b/locales/rn/lv.json @@ -670,7 +670,8 @@ "remove_condition": "Noņemt nosacījumu", "sieve_editor": { "line_count": "{count, plural, zero {# rindas} one {# rinda} other {# rindas}}" - } + }, + "validation_header_name": "Ievadiet galvenes nosaukumu, kurā ir tikai burti A–Z, cipari, defises un pasvītras, piemēram, X-Spam-Flag." }, "folders": { "account": "Konts", diff --git a/locales/rn/mn.json b/locales/rn/mn.json index a6982fee..6a4b8f42 100644 --- a/locales/rn/mn.json +++ b/locales/rn/mn.json @@ -670,7 +670,8 @@ "remove_condition": "Нөхцөлийг хасах", "sieve_editor": { "line_count": "{count, plural, one {# мөр} other {# мөр}}" - } + }, + "validation_header_name": "Зөвхөн A-Z үсэг, тоо, зураас, доогуур зураас агуулсан толгойн нэр оруулна уу, жишээ нь X-Spam-Flag." }, "folders": { "account": "Бүртгэл", diff --git a/locales/rn/nb.json b/locales/rn/nb.json index 2cfc5254..5e699dee 100644 --- a/locales/rn/nb.json +++ b/locales/rn/nb.json @@ -670,7 +670,8 @@ "remove_condition": "Fjern vilkår", "sieve_editor": { "line_count": "{count, plural, one {# linje} other {# linjer}}" - } + }, + "validation_header_name": "Skriv inn et navn på meldingshode som bare bruker bokstavene A–Z, sifre, bindestreker og understreker, for eksempel X-Spam-Flag." }, "folders": { "account": "Konto", diff --git a/locales/rn/nl.json b/locales/rn/nl.json index 4a2d637c..4e6ed771 100644 --- a/locales/rn/nl.json +++ b/locales/rn/nl.json @@ -670,7 +670,8 @@ "remove_condition": "Voorwaarde verwijderen", "sieve_editor": { "line_count": "{count, plural, one {# regel} other {# regels}}" - } + }, + "validation_header_name": "Voer een headernaam in met alleen de letters A-Z, cijfers, koppeltekens en underscores, zoals X-Spam-Flag." }, "folders": { "account": "Account", diff --git a/locales/rn/pl.json b/locales/rn/pl.json index d4b04ff2..ab2883a4 100644 --- a/locales/rn/pl.json +++ b/locales/rn/pl.json @@ -670,7 +670,8 @@ "remove_condition": "Usuń warunek", "sieve_editor": { "line_count": "{count, plural, one {# wiersz} few {# wiersze} many {# wierszy} other {# wiersza}}" - } + }, + "validation_header_name": "Wpisz nazwę nagłówka, która zawiera tylko litery A–Z, cyfry, łączniki i podkreślenia, na przykład X-Spam-Flag." }, "folders": { "account": "Konto", diff --git a/locales/rn/pt.json b/locales/rn/pt.json index 65560307..eacd333e 100644 --- a/locales/rn/pt.json +++ b/locales/rn/pt.json @@ -670,7 +670,8 @@ "remove_condition": "Remover condição", "sieve_editor": { "line_count": "{count, plural, one {# linha} other {# linhas}}" - } + }, + "validation_header_name": "Insira um nome de cabeçalho que use apenas as letras A-Z, algarismos, hífens e sublinhados, como X-Spam-Flag." }, "folders": { "account": "Conta", diff --git a/locales/rn/ro.json b/locales/rn/ro.json index 7190f4b7..b464c6d5 100644 --- a/locales/rn/ro.json +++ b/locales/rn/ro.json @@ -670,7 +670,8 @@ "remove_condition": "Eliminați condiția", "sieve_editor": { "line_count": "{count, plural, one {# linie} few {# linii} other {# de linii}}" - } + }, + "validation_header_name": "Introduceți un nume de antet care folosește doar literele A–Z, cifre, cratime și liniuțe de subliniere, de exemplu X-Spam-Flag." }, "folders": { "account": "Cont", diff --git a/locales/rn/ru.json b/locales/rn/ru.json index ad8ccafe..20374153 100644 --- a/locales/rn/ru.json +++ b/locales/rn/ru.json @@ -670,7 +670,8 @@ "remove_condition": "Удалить условие", "sieve_editor": { "line_count": "{count, plural, one {# строка} few {# строки} many {# строк} other {# строки}}" - } + }, + "validation_header_name": "Введите имя заголовка, состоящее только из латинских букв A–Z, цифр, дефисов и подчёркиваний, например X-Spam-Flag." }, "folders": { "account": "Аккаунт", diff --git a/locales/rn/sk.json b/locales/rn/sk.json index 5fdeda48..93618438 100644 --- a/locales/rn/sk.json +++ b/locales/rn/sk.json @@ -670,7 +670,8 @@ "remove_condition": "Odstrániť podmienku", "sieve_editor": { "line_count": "{count, plural, one {# riadok} few {# riadky} many {# riadka} other {# riadkov}}" - } + }, + "validation_header_name": "Zadajte názov hlavičky, ktorý obsahuje iba písmená A – Z, číslice, spojovníky a podčiarkovníky, napríklad X-Spam-Flag." }, "folders": { "account": "Účet", diff --git a/locales/rn/tr.json b/locales/rn/tr.json index 6953b206..74efb160 100644 --- a/locales/rn/tr.json +++ b/locales/rn/tr.json @@ -670,7 +670,8 @@ "remove_condition": "Koşulu kaldır", "sieve_editor": { "line_count": "{count, plural, one {# satır} other {# satır}}" - } + }, + "validation_header_name": "Yalnızca A-Z harfleri, rakamlar, kısa çizgiler ve alt çizgiler içeren bir başlık adı girin; örneğin X-Spam-Flag." }, "folders": { "account": "Hesap", diff --git a/locales/rn/uk.json b/locales/rn/uk.json index 78b6d897..3c178491 100644 --- a/locales/rn/uk.json +++ b/locales/rn/uk.json @@ -670,7 +670,8 @@ "remove_condition": "Видалити умову", "sieve_editor": { "line_count": "{count, plural, one {# рядок} few {# рядки} many {# рядків} other {# рядка}}" - } + }, + "validation_header_name": "Введіть назву заголовка, що містить лише латинські літери A–Z, цифри, дефіси та підкреслення, наприклад X-Spam-Flag." }, "folders": { "account": "Обліковий запис", diff --git a/locales/rn/zh-TW.json b/locales/rn/zh-TW.json index a5b2d754..96826eef 100644 --- a/locales/rn/zh-TW.json +++ b/locales/rn/zh-TW.json @@ -670,7 +670,8 @@ "remove_condition": "移除條件", "sieve_editor": { "line_count": "{count, plural, other {# 行}}" - } + }, + "validation_header_name": "請輸入僅包含字母 A-Z、數字、連字號和底線的標頭名稱,例如 X-Spam-Flag。" }, "folders": { "account": "帳號", diff --git a/locales/rn/zh.json b/locales/rn/zh.json index 248a6ef0..3f06eace 100644 --- a/locales/rn/zh.json +++ b/locales/rn/zh.json @@ -670,7 +670,8 @@ "remove_condition": "移除条件", "sieve_editor": { "line_count": "{count, plural, other {# 行}}" - } + }, + "validation_header_name": "请输入仅包含字母 A-Z、数字、连字符和下划线的标头名称,例如 X-Spam-Flag。" }, "folders": { "account": "账户", diff --git a/src/components/filters/FilterRuleModal.tsx b/src/components/filters/FilterRuleModal.tsx index 7128ac31..9ff9115f 100644 --- a/src/components/filters/FilterRuleModal.tsx +++ b/src/components/filters/FilterRuleModal.tsx @@ -28,6 +28,7 @@ import { updateFilterAction, withMailboxTarget, } from '../../lib/sieve/rule-actions'; +import { isValidSieveHeaderName } from '../../lib/sieve/generator'; import type { Mailbox } from '../../api/types'; import type { FilterRule, @@ -197,13 +198,24 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F .map((cond) => { if (isHasAny(cond)) return { ...cond, value: '' }; if (cond.field === 'size') return cond; // numeric, single-value only - if (typeof cond.value !== 'string') return cond; // already structured - return { ...cond, value: inputStringToValue(cond.value) }; + // A keyboard's trailing space is never part of a header name. + const named = cond.field === 'header' ? { ...cond, headerName: cond.headerName?.trim() } : cond; + if (typeof named.value !== 'string') return named; // already structured + return { ...named, value: inputStringToValue(named.value) }; }); if (validConditions.length === 0) { Alert.alert(t('settings.filters.validation_empty_conditions', 'At least one condition with a value is required')); return; } + // The server refuses the whole filter script over one header name it + // can't read (a space, a dot or a colon in it), so say so here. + if (validConditions.some((cond) => cond.field === 'header' && !isValidSieveHeaderName(cond.headerName ?? ''))) { + Alert.alert(t( + 'settings.filters.validation_header_name', + 'Enter a header name that uses only the letters A-Z, digits, hyphens and underscores, such as X-Spam-Flag.', + )); + return; + } const validActions = actions .map((a) => withMailboxTarget(a, mailboxTargets)) .filter((a) => !ACTIONS_WITH_VALUE.has(a.type) || a.value?.trim()); @@ -313,6 +325,8 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F value={condition.headerName || ''} onChangeText={(v) => updateCondition(index, { headerName: v })} placeholder={t('settings.filters.header_name', 'Header name')} + autoCapitalize="none" + autoCorrect={false} /> )} diff --git a/src/lib/sieve/__tests__/generator.test.ts b/src/lib/sieve/__tests__/generator.test.ts index da69f7f3..737e2218 100644 --- a/src/lib/sieve/__tests__/generator.test.ts +++ b/src/lib/sieve/__tests__/generator.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { generateScript } from '../generator'; +import { generateScript, isValidSieveHeaderName } from '../generator'; import { parseScript } from '../parser'; import type { FilterRule } from '../types'; @@ -471,6 +471,88 @@ describe('generateScript', () => { }); }); + // The rules ride in a `/* */` comment, which the first `*/` ends, and each + // rule's code starts with a `# Rule:` comment, which a line break ends. + describe('text that could break out of its comment', () => { + const tricky = makeRule({ + id: 'rule-*/', + name: 'Newsletters */ and more', + conditions: [ + { field: 'subject', comparator: 'contains', value: ['*/ spam', 'end */ here'] }, + { field: 'header', comparator: 'is', value: 'a*/b', headerName: 'X-Spam-Flag' }, + ], + actions: [{ type: 'move', value: 'News */ letters' }, { type: 'mark_read' }], + }); + const vacation = { isEnabled: true, subject: 'Away */', textBody: 'Back soon */ really' }; + + it('keeps */ inside the metadata comment and reads every value back', () => { + const script = generateScript([tricky], vacation); + + // The comment's first */ is its own end marker. + const begin = script.indexOf('/* @metadata:begin'); + expect(script.indexOf('*/', begin)).toBe(script.indexOf('@metadata:end */') + '@metadata:end '.length); + const parsed = parseScript(script); + expect(parsed.isOpaque).toBe(false); + expect(parsed.rules).toEqual([tricky]); + expect(parsed.vacation).toEqual(vacation); + }); + + it('writes an escape that JSON, and so every older client, reads back as */', () => { + const script = generateScript([makeRule({ name: 'a */ b' })]); + + const json = script.match(/@metadata:begin\n(.*)\n@metadata:end/)![1]; + expect(json).toContain('"name":"a *\\/ b"'); + expect(JSON.parse(json).rules[0].name).toBe('a */ b'); + }); + + it('keeps a line break in a rule name out of the script and still recognises the block', () => { + const rule = makeRule({ name: 'Line one\r\nline two\nthree' }); + const script = generateScript([rule]); + + expect(script).toContain('\n# Rule: Line one line two three\nif '); + const parsed = parseScript(script); + // Not kept a second time as an external rule. + expect(parsed.rules).toEqual([rule]); + expect(parseScript(generateScript(parsed.rules, undefined, { externalRequires: parsed.externalRequires })).rules) + .toEqual([rule]); + }); + }); + + // sieve-rs (Stalwart 0.16 and 1.0) refuses the whole script over one of + // these, the auto-reply and every other rule with it. + describe('header names the server would refuse', () => { + it.each(['X-Spam Flag', 'X-Foo.Bar', 'X-Foo:', 'X-"Quoted"', 'Grüße'])( + 'skips a rule that tests %j and keeps it in the metadata', + (headerName) => { + const bad = makeRule({ + id: 'bad', + name: 'Bad', + conditions: [{ field: 'header', comparator: 'contains', value: 'yes', headerName }], + }); + const good = makeRule({ id: 'good', name: 'Good' }); + + const script = generateScript([bad, good]); + + const code = script.slice(script.indexOf('@metadata:end */')); + expect(code).not.toContain('# Rule: Bad'); + expect(code).toContain('# Rule: Good'); + expect(parseScript(script).rules).toEqual([bad, good]); + }, + ); + + it('accepts letters, digits, hyphens and underscores, blanks around them aside', () => { + expect(isValidSieveHeaderName('X-Spam_Flag2')).toBe(true); + expect(isValidSieveHeaderName(' X-Spam-Flag ')).toBe(true); + expect(isValidSieveHeaderName('')).toBe(false); + expect(isValidSieveHeaderName('X Spam')).toBe(false); + + const script = generateScript([makeRule({ + conditions: [{ field: 'header', comparator: 'contains', value: 'yes', headerName: ' X-Spam-Flag ' }], + })]); + expect(script).toContain('if header :contains "X-Spam-Flag" "yes" {'); + }); + }); + describe('round-trip', () => { it('preserves rules through generate → parse cycle', () => { const rules: FilterRule[] = [ diff --git a/src/lib/sieve/generator.ts b/src/lib/sieve/generator.ts index 0d97c19a..d1b4d158 100644 --- a/src/lib/sieve/generator.ts +++ b/src/lib/sieve/generator.ts @@ -22,6 +22,29 @@ function escapeString(value: string): string { return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } +// What sieve-rs (Stalwart 0.16 and 1.0) accepts as a header name. Anything +// else - a space, a dot, a colon - makes the server refuse the whole script. +const HEADER_NAME = /^[A-Za-z0-9_-]+$/; + +/** Whether `name` can go into a Sieve `header` test (surrounding blanks aside). */ +export function isValidSieveHeaderName(name: string): boolean { + return HEADER_NAME.test(name.trim()); +} + +/** + * A rule's name as it appears in its `# Rule:` comment, which ends at the + * line break: a CR or LF in the name would have turned the rest of it into + * script. The parser matches the comment back to the rule by this form. + */ +export function ruleCommentName(name: string): string { + return name.replace(/[\r\n]+/g, ' ').trim(); +} + +/** A `header` condition whose name the server would refuse. */ +function hasInvalidHeaderName(condition: FilterCondition): boolean { + return condition.field === 'header' && !!condition.headerName?.trim() && !isValidSieveHeaderName(condition.headerName); +} + // Normalise the condition value to a non-empty string array. Single-value // conditions stay one-element; arrays are filtered for empty strings. function toValueList(value: string | string[]): string[] { @@ -78,7 +101,7 @@ function generateCondition(condition: FilterCondition): string { } const headerName = field === 'header' - ? (condition.headerName || 'X-Unknown') + ? (condition.headerName?.trim() || 'X-Unknown') : HEADER_MAP[field]; // A field this client does not know (a rule authored by a newer webmail) @@ -290,7 +313,10 @@ export function generateScript( if (options.includeVacation) { metadata.includeVacation = true; } - const metadataJson = JSON.stringify(metadata); + // The rules ride in a `/* */` comment, which the first `*/` ends: one in a + // rule name or value cut the metadata short and turned the rest into + // script. JSON reads `\/` back as `/`, so every client parses it as before. + const metadataJson = JSON.stringify(metadata).replace(/\*\//g, '*\\/'); const lines: string[] = []; lines.push('/* @metadata:begin'); @@ -334,9 +360,16 @@ export function generateScript( console.warn(`[filters] Skipping rule "${rule.name}": empty conditions or actions`); continue; } + // The server would refuse the whole script, every other rule and the + // auto-reply with it. Dropping just the condition could widen an "all + // of" rule, so the rule sits out; the metadata keeps it for editing. + if (rule.conditions.some(hasInvalidHeaderName)) { + console.warn(`[filters] Skipping rule "${rule.name}": a header name the server would refuse`); + continue; + } lines.push(''); - lines.push(`# Rule: ${rule.name}`); + lines.push(`# Rule: ${ruleCommentName(rule.name)}`); const conditions = rule.conditions.map(generateCondition); let conditionStr: string; diff --git a/src/lib/sieve/parser.ts b/src/lib/sieve/parser.ts index 57445a13..0439ad30 100644 --- a/src/lib/sieve/parser.ts +++ b/src/lib/sieve/parser.ts @@ -7,6 +7,7 @@ import type { FilterRule, VacationSieveConfig, } from './types'; +import { ruleCommentName } from './generator'; // Ported from the webmail's lib/sieve/parser.ts. Parses a Sieve script back // into structured rules. Bulwark-authored scripts carry an `@metadata` JSON @@ -838,7 +839,8 @@ export function parseScript(content: string): ParseResult { const match = raw.match(/#\s*Rule:\s*(.+?)\s*$/m); if (match) { const name = match[1].trim(); - if (bulwarkRules.some(b => b.name === name)) return false; + // The comment carries the name without line breaks (ruleCommentName). + if (bulwarkRules.some(b => ruleCommentName(b.name) === name)) return false; } if (/#\s*Vacation auto-reply/i.test(raw)) return false; return true;