mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: save filters whose rules contain */, line breaks or odd header names
The app stores its filter rules as JSON inside a `/* */` comment at the top of the Sieve script. A `*/` anywhere in a rule name, a value or the auto-reply text ended that comment early and turned the rest of the rules into script, and a CR or LF in a rule name did the same to its `# Rule:` comment. Stalwart (sieve-rs, on 0.16 and 1.0) also accepts only the letters A-Z, digits, `-` and `_` in a header name. Any of these made the server refuse the whole script, so saving any filter, and the auto-reply with it, failed. - `*/` in the embedded rules is written as `*\/`, which JSON reads back as `*/`, so every client parses the rules as before - the `# Rule:` comment carries the rule name without line breaks, and the parser matches the comment back to its rule by that form - the filter editor trims header names, turns autocorrect off for them and refuses to save one the server would reject, with a translated message; the generator leaves a rule with such a header name out of the script, since dropping only the condition could widen an "all of" rule, and keeps it in the stored rules for editing
This commit is contained in:
+2
-1
@@ -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": "الحساب",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -670,7 +670,8 @@
|
||||
"remove_condition": "حذف شرط",
|
||||
"sieve_editor": {
|
||||
"line_count": "{count, plural, one {# خط} other {# خط}}"
|
||||
}
|
||||
},
|
||||
"validation_header_name": "نام هدری وارد کنید که فقط از حروف A-Z، رقم، خط تیره و زیرخط تشکیل شده باشد، مانند X-Spam-Flag."
|
||||
},
|
||||
"folders": {
|
||||
"account": "حساب",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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": "חשבון",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -670,7 +670,8 @@
|
||||
"remove_condition": "条件を削除",
|
||||
"sieve_editor": {
|
||||
"line_count": "{count, plural, other {#行}}"
|
||||
}
|
||||
},
|
||||
"validation_header_name": "ヘッダー名には英字(A~Z)、数字、ハイフン、アンダースコアのみを使用してください(例: X-Spam-Flag)。"
|
||||
},
|
||||
"folders": {
|
||||
"account": "アカウント",
|
||||
|
||||
+2
-1
@@ -670,7 +670,8 @@
|
||||
"remove_condition": "조건 제거",
|
||||
"sieve_editor": {
|
||||
"line_count": "{count, plural, other {#줄}}"
|
||||
}
|
||||
},
|
||||
"validation_header_name": "헤더 이름은 영문자(A-Z), 숫자, 하이픈, 밑줄만 사용해 입력해 주세요(예: X-Spam-Flag)."
|
||||
},
|
||||
"folders": {
|
||||
"account": "계정",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -670,7 +670,8 @@
|
||||
"remove_condition": "Нөхцөлийг хасах",
|
||||
"sieve_editor": {
|
||||
"line_count": "{count, plural, one {# мөр} other {# мөр}}"
|
||||
}
|
||||
},
|
||||
"validation_header_name": "Зөвхөн A-Z үсэг, тоо, зураас, доогуур зураас агуулсан толгойн нэр оруулна уу, жишээ нь X-Spam-Flag."
|
||||
},
|
||||
"folders": {
|
||||
"account": "Бүртгэл",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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": "Аккаунт",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+2
-1
@@ -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": "Обліковий запис",
|
||||
|
||||
@@ -670,7 +670,8 @@
|
||||
"remove_condition": "移除條件",
|
||||
"sieve_editor": {
|
||||
"line_count": "{count, plural, other {# 行}}"
|
||||
}
|
||||
},
|
||||
"validation_header_name": "請輸入僅包含字母 A-Z、數字、連字號和底線的標頭名稱,例如 X-Spam-Flag。"
|
||||
},
|
||||
"folders": {
|
||||
"account": "帳號",
|
||||
|
||||
+2
-1
@@ -670,7 +670,8 @@
|
||||
"remove_condition": "移除条件",
|
||||
"sieve_editor": {
|
||||
"line_count": "{count, plural, other {# 行}}"
|
||||
}
|
||||
},
|
||||
"validation_header_name": "请输入仅包含字母 A-Z、数字、连字符和下划线的标头名称,例如 X-Spam-Flag。"
|
||||
},
|
||||
"folders": {
|
||||
"account": "账户",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user