From 4f5e667a533c71448dd1ddaf3ca06b4530346a9b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:29:30 +0200 Subject: [PATCH] fix: keep filter scripts valid when a rule contains */ or a bad header name The rule metadata is JSON inside a Sieve bracket comment, so a `*/` in a rule name, a value or the vacation text ended the comment early and broke the script. A line break in a rule name escaped the `# Rule:` comment line. And a header name outside `[A-Za-z0-9_-]` made Stalwart (0.16 and 1.0) refuse the whole script, every other rule with it. - `*/` in the metadata JSON is written as `*\/`, which `JSON.parse` reads back as `/`, so older versions still parse it. - Line breaks in a rule name are flattened on the `# Rule:` line, and the parser matches its own blocks the same way. - The rule editor refuses to save an invalid header name, with a translated message. The generator skips such a rule (it stays in the metadata, so it can be fixed) instead of writing a script the server refuses. --- .../__tests__/filter-rule-modal.test.tsx | 64 +++++++++++++++++ components/filters/filter-rule-modal.tsx | 10 ++- lib/sieve/__tests__/generator.test.ts | 69 ++++++++++++++++++- lib/sieve/__tests__/parser.test.ts | 39 +++++++++++ lib/sieve/generator.ts | 49 +++++++++++-- lib/sieve/parser.ts | 4 +- locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nb/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh-TW/common.json | 1 + locales/zh/common.json | 1 + 33 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 components/filters/__tests__/filter-rule-modal.test.tsx diff --git a/components/filters/__tests__/filter-rule-modal.test.tsx b/components/filters/__tests__/filter-rule-modal.test.tsx new file mode 100644 index 000000000..a71b909fb --- /dev/null +++ b/components/filters/__tests__/filter-rule-modal.test.tsx @@ -0,0 +1,64 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { FilterRule } from '@/lib/jmap/sieve-types'; +import { toast } from '@/stores/toast-store'; +import { FilterRuleModal } from '../filter-rule-modal'; + +vi.mock('@/stores/toast-store', () => ({ + toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }, +})); + +function headerRule(headerName: string): FilterRule { + return { + id: 'rule-1', + name: 'Spam flag', + enabled: true, + matchType: 'all', + conditions: [{ field: 'header', comparator: 'contains', value: 'yes', headerName }], + actions: [{ type: 'discard' }], + stopProcessing: false, + }; +} + +// Stalwart's Sieve compiler (sieve-rs, on 0.16 and 1.0) refuses a header name +// outside [A-Za-z0-9_-], and with it the whole script, so every other rule +// would stop running too. The modal keeps such a rule from being saved. +describe('FilterRuleModal header names', () => { + beforeEach(() => { + vi.mocked(toast.error).mockClear(); + }); + + it.each(['X-Spam Flag', 'X-Foo.Bar', ''])('blocks saving the header name %j', (headerName) => { + const onSave = vi.fn(); + render( {}} />); + + fireEvent.click(screen.getByText('save')); + + expect(onSave).not.toHaveBeenCalled(); + expect(toast.error).toHaveBeenCalledWith('validation_header_name'); + }); + + it('saves a valid header name, trimmed', () => { + const onSave = vi.fn(); + render( {}} />); + + fireEvent.click(screen.getByText('save')); + + expect(toast.error).not.toHaveBeenCalled(); + expect(onSave).toHaveBeenCalledTimes(1); + expect(onSave.mock.calls[0][0].conditions).toEqual([ + { field: 'header', comparator: 'contains', value: 'yes', headerName: 'X-Spam_Flag-2' }, + ]); + }); + + it('blocks a name typed into the header field', () => { + const onSave = vi.fn(); + render( {}} />); + + fireEvent.change(screen.getByPlaceholderText('header_name'), { target: { value: 'X-Mailer: Foo' } }); + fireEvent.click(screen.getByText('save')); + + expect(onSave).not.toHaveBeenCalled(); + expect(toast.error).toHaveBeenCalledWith('validation_header_name'); + }); +}); diff --git a/components/filters/filter-rule-modal.tsx b/components/filters/filter-rule-modal.tsx index 7c673e1aa..5181ba5e4 100644 --- a/components/filters/filter-rule-modal.tsx +++ b/components/filters/filter-rule-modal.tsx @@ -16,6 +16,7 @@ import type { FilterActionType, } from "@/lib/jmap/sieve-types"; import type { Mailbox } from "@/lib/jmap/types"; +import { isValidSieveHeaderName } from "@/lib/sieve/generator"; import { buildMailboxTree, flattenMailboxTree, type MailboxNode, generateUUID } from "@/lib/utils"; import { useSettingsStore } from "@/stores/settings-store"; import { useKeywordFormat } from "@/hooks/use-keyword-format"; @@ -165,11 +166,18 @@ export function FilterRuleModal({ if (typeof c.value !== "string") return c; // already structured const parsed = inputStringToValue(c.value); return { ...c, value: parsed }; - }); + }) + .map((c) => (c.field === "header" ? { ...c, headerName: (c.headerName ?? "").trim() } : c)); if (validConditions.length === 0) { toast.error(t("validation_empty_conditions")); return; } + // Stalwart's Sieve compiler refuses any other header name, and with it + // the whole script, so every other rule would stop working too. + if (validConditions.some((c) => c.field === "header" && !isValidSieveHeaderName(c.headerName ?? ""))) { + toast.error(t("validation_header_name")); + return; + } const validActions = actions .map((a) => { diff --git a/lib/sieve/__tests__/generator.test.ts b/lib/sieve/__tests__/generator.test.ts index 6ad885628..b30bc3fb8 100644 --- a/lib/sieve/__tests__/generator.test.ts +++ b/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 '@/lib/jmap/sieve-types'; @@ -42,6 +42,40 @@ describe('generateScript', () => { expect(script).toContain('fileinto "Archive";'); }); + describe('text that could break out of a comment', () => { + it('escapes */ inside the metadata comment so the JSON cannot end it early', () => { + const script = generateScript( + [makeRule({ + name: 'Invoices */ keep', + conditions: [{ field: 'subject', comparator: 'contains', value: ['a*/b', '*/'] }], + actions: [{ type: 'move', value: 'Bills*/2026' }], + })], + { isEnabled: true, subject: 'Away */', textBody: 'Back soon */ promise' }, + ); + const comment = script.slice(script.indexOf('/* @metadata:begin'), script.indexOf('@metadata:end */')); + expect(comment).not.toContain('*/'); + expect(comment).toContain('Invoices *\\/ keep'); + // `\/` is a JSON escape, so the JSON reads back exactly as written. + const json = script.match(/@metadata:begin\n(.*)\n@metadata:end/)![1]; + const metadata = JSON.parse(json); + expect(metadata.rules[0].name).toBe('Invoices */ keep'); + expect(metadata.rules[0].conditions[0].value).toEqual(['a*/b', '*/']); + expect(metadata.vacation.textBody).toBe('Back soon */ promise'); + }); + + it('keeps the metadata byte-identical when nothing needs escaping', () => { + const rules = [makeRule({ name: 'Plain / slash and * star' })]; + const json = generateScript(rules).match(/@metadata:begin\n(.*)\n@metadata:end/)![1]; + expect(json).toBe(JSON.stringify({ version: 1, rules })); + }); + + it('keeps a line break in the rule name out of the # Rule: line', () => { + const script = generateScript([makeRule({ name: 'Line one\r\ndiscard;\nstop' })]); + expect(script).toContain('# Rule: Line one discard; stop\n'); + expect(script).not.toMatch(/^discard;$/m); + }); + }); + describe('condition fields', () => { it('maps to field to "To" header', () => { const script = generateScript([makeRule({ @@ -71,6 +105,39 @@ describe('generateScript', () => { expect(script).toContain('header :contains "X-Custom" "test"'); }); + // sieve-rs (Stalwart 0.16 and 1.0) accepts only [A-Za-z0-9_-]+ and refuses + // the whole script otherwise, taking every other rule down with it. + it.each(['X-Spam Flag', 'X-Foo.Bar', 'X-Foo:', 'X-"Quoted"', 'Überschrift'])( + 'skips a rule whose header name %j Stalwart would refuse, keeping the others', + (headerName) => { + const script = generateScript([ + makeRule({ id: 'bad', name: 'Bad header', conditions: [{ field: 'header', comparator: 'contains', value: 'yes', headerName }] }), + makeRule({ id: 'good', name: 'Good rule' }), + ]); + const code = script.slice(script.indexOf('@metadata:end */')); + expect(code).not.toContain('Bad header'); + expect(code).not.toContain('"yes"'); + expect(code).toContain('# Rule: Good rule'); + // Kept in the metadata, so the rule can still be corrected in the UI. + expect(parseScript(script).rules.map((r) => r.id)).toEqual(['bad', 'good']); + }, + ); + + it('accepts letters, digits, hyphens and underscores, trimming stray spaces', () => { + const script = generateScript([makeRule({ + conditions: [{ field: 'header', comparator: 'is', value: 'yes', headerName: ' X-Spam_Flag-2 ' }], + })]); + expect(script).toContain('header :is "X-Spam_Flag-2" "yes"'); + }); + + it('validates header names with the same rule the server applies', () => { + expect(isValidSieveHeaderName('X-Spam-Flag')).toBe(true); + expect(isValidSieveHeaderName('x_custom_2')).toBe(true); + expect(isValidSieveHeaderName('')).toBe(false); + expect(isValidSieveHeaderName('X-Spam Flag')).toBe(false); + expect(isValidSieveHeaderName('X-Foo.Bar')).toBe(false); + }); + it('handles size greater_than', () => { const script = generateScript([makeRule({ conditions: [{ field: 'size', comparator: 'greater_than', value: '1000000' }], diff --git a/lib/sieve/__tests__/parser.test.ts b/lib/sieve/__tests__/parser.test.ts index 3d6af5484..e4d84207e 100644 --- a/lib/sieve/__tests__/parser.test.ts +++ b/lib/sieve/__tests__/parser.test.ts @@ -25,6 +25,45 @@ describe('parseScript', () => { expect(result.rules).toEqual(rules); }); + it('round-trips names and values containing */, which would end the metadata comment', () => { + const rules = [ + makeRule({ + id: 'r1', + name: 'Receipts */ and more', + conditions: [ + { field: 'subject', comparator: 'contains', value: ['*/', 'a */ b'] }, + { field: 'body', comparator: 'contains', value: 'end */ of comment' }, + ], + actions: [{ type: 'add_label', value: 'bills' }], + }), + makeRule({ id: 'r2', name: 'Second' }), + ]; + const vacation = { isEnabled: true, subject: 'Out */', textBody: 'Reply */ later' }; + + const result = parseScript(generateScript(rules, vacation)); + + expect(result.isOpaque).toBe(false); + expect(result.rules).toEqual(rules); + expect(result.vacation).toEqual(vacation); + }); + + it('reads back metadata a newer Bulwark wrote with */ escaped (older clients parse it too)', () => { + // JSON.parse turns `\/` back into `/`, so no reader needs to know. + const script = '/* @metadata:begin\n{"version":1,"rules":[{"id":"x","name":"a *\\/ b","enabled":true,"matchType":"all",' + + '"conditions":[{"field":"from","comparator":"contains","value":"c"}],"actions":[{"type":"discard"}],"stopProcessing":false}]}\n@metadata:end */\n'; + expect(parseScript(script).rules[0].name).toBe('a */ b'); + }); + + it('recognises its own rule block when the rule name had a line break', () => { + // The # Rule: line flattens the break; the block must still be matched to + // the rule, or the next save would copy it as an "external" rule. + const rules = [makeRule({ name: 'Two\nlines' })]; + const result = parseScript(generateScript(rules)); + + expect(result.rules).toEqual(rules); + expect(result.rules.some((r) => r.origin === 'external')).toBe(false); + }); + it('parses external rules when no Bulwark metadata is present', () => { const result = parseScript('require ["fileinto"];\nif header :contains "From" "x" { fileinto "Y"; }'); expect(result.isOpaque).toBe(false); diff --git a/lib/sieve/generator.ts b/lib/sieve/generator.ts index ae77d849c..378a2ddf0 100644 --- a/lib/sieve/generator.ts +++ b/lib/sieve/generator.ts @@ -12,6 +12,33 @@ function escapeString(value: string): string { return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } +/** + * Header names Stalwart's Sieve compiler accepts (sieve-rs, 0.16 and 1.0). + * Anything else - a space, a dot, a colon - fails the whole script with + * "Invalid header name". + */ +const SIEVE_HEADER_NAME_RE = /^[A-Za-z0-9_-]+$/; + +export function isValidSieveHeaderName(name: string): boolean { + return SIEVE_HEADER_NAME_RE.test(name); +} + +/** The header a condition tests; a header condition without a name tests X-Unknown. */ +function conditionHeaderName(condition: FilterCondition): string { + return condition.field === 'header' + ? (condition.headerName?.trim() || 'X-Unknown') + : HEADER_MAP[condition.field]; +} + +/** + * A rule name as it appears on the `# Rule:` comment line. A hash comment + * ends at the line break, so one inside the name would turn the rest into + * script code. + */ +export function ruleCommentName(name: string): string { + return name.replace(/[\r\n]+/g, ' '); +} + // 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[] { @@ -67,9 +94,7 @@ function generateCondition(condition: FilterCondition): string { return `header :mime :anychild :matches ["Content-Disposition", "Content-Type"] ${formatStringArg(normalised, (ext) => `*.${ext}*`)}`; } - const headerName = field === 'header' - ? (condition.headerName || 'X-Unknown') - : HEADER_MAP[field]; + const headerName = conditionHeaderName(condition); switch (comparator) { case 'contains': @@ -272,7 +297,11 @@ export function generateScript( if (options.includeVacation) { metadata.includeVacation = true; } - const metadataJson = JSON.stringify(metadata); + // The JSON sits in a bracket comment, which the first `*/` ends - one in a + // rule name, a value or the vacation text would leak the rest of the JSON + // into the script. `\/` is a valid JSON escape for `/`, so every JSON + // parser, older Bulwark versions included, reads it back unchanged. + const metadataJson = JSON.stringify(metadata).replace(/\*\//g, '*\\/'); const lines: string[] = []; lines.push('/* @metadata:begin'); @@ -316,9 +345,19 @@ export function generateScript( debug.warn('filters', `Skipping rule "${rule.name}": empty conditions or actions`); continue; } + // The server would refuse the whole script, every other rule with it. + // The rule stays in the metadata, so it can still be corrected. + const badHeader = rule.conditions + .filter(c => c.field === 'header') + .map(conditionHeaderName) + .find(name => !isValidSieveHeaderName(name)); + if (badHeader !== undefined) { + debug.warn('filters', `Skipping rule "${rule.name}": invalid header name "${badHeader}"`); + 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/lib/sieve/parser.ts b/lib/sieve/parser.ts index 995bfba0b..8348437a0 100644 --- a/lib/sieve/parser.ts +++ b/lib/sieve/parser.ts @@ -8,6 +8,7 @@ import type { VacationSieveConfig, } from '@/lib/jmap/sieve-types'; import { debug } from '@/lib/debug'; +import { ruleCommentName } from '@/lib/sieve/generator'; export interface ParseResult { rules: FilterRule[]; @@ -834,7 +835,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 line carries the name with its line breaks flattened. + if (bulwarkRules.some(b => ruleCommentName(b.name).trim() === name)) return false; } if (/#\s*Vacation auto-reply/i.test(raw)) return false; return true; diff --git a/locales/ar/common.json b/locales/ar/common.json index 66f299a8e..2ac3d4870 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "اسم القاعدة مطلوب", "validation_empty_conditions": "مطلوب شرط واحد على الأقل بقيمة", "validation_empty_actions": "مطلوب إجراء واحد على الأقل", + "validation_header_name": "يمكن أن يحتوي اسم الترويسة على الأحرف A-Z والأرقام والشرطات والشرطات السفلية فقط", "templates_section": "البدء من قالب", "template_newsletters": "نقل النشرات البريدية إلى مجلد", "template_receipts": "أرشفة الإيصالات تلقائيًا", diff --git a/locales/ca/common.json b/locales/ca/common.json index cc1001d2a..27c579412 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "El nom de la regla és obligatori", "validation_empty_conditions": "Cal com a mínim una condició amb un valor", "validation_empty_actions": "Cal com a mínim una acció", + "validation_header_name": "Els noms de capçalera només poden contenir les lletres A-Z, xifres, guionets i guions baixos", "templates_section": "Comença des d'una plantilla", "template_newsletters": "Mou els butlletins a una carpeta", "template_receipts": "Arxiva automàticament els rebuts", diff --git a/locales/cs/common.json b/locales/cs/common.json index 53ff0b59b..e6a538afe 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Název pravidla je vyžadován", "validation_empty_conditions": "Je vyžadována alespoň jedna podmínka s hodnotou", "validation_empty_actions": "Je vyžadována alespoň jedna akce", + "validation_header_name": "Názvy hlaviček smí obsahovat jen písmena A-Z, číslice, pomlčky a podtržítka", "templates_section": "Začít ze šablony", "template_newsletters": "Přesunout newslettery do složky", "template_receipts": "Automaticky archivovat účtenky", diff --git a/locales/da/common.json b/locales/da/common.json index 9249d99ec..a41208471 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2087,6 +2087,7 @@ "validation_empty_name": "Regelnavn er påkrævet", "validation_empty_conditions": "Mindst én betingelse med en værdi er påkrævet", "validation_empty_actions": "Mindst én handling er påkrævet", + "validation_header_name": "Headernavne må kun indeholde bogstaverne A-Z, cifre, bindestreger og understregninger", "templates_section": "Start fra skabelon", "template_newsletters": "Flyt nyhedsbreve til mappe", "template_receipts": "Auto-arkiver kvitteringer", diff --git a/locales/de/common.json b/locales/de/common.json index c8a28ed35..c0163b3e9 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Regelname ist erforderlich", "validation_empty_conditions": "Mindestens eine Bedingung mit einem Wert ist erforderlich", "validation_empty_actions": "Mindestens eine Aktion ist erforderlich", + "validation_header_name": "Header-Namen dürfen nur die Buchstaben A-Z, Ziffern, Bindestriche und Unterstriche enthalten", "templates_section": "Von Vorlage starten", "template_newsletters": "Newsletter in Ordner verschieben", "template_receipts": "Quittungen automatisch archivieren", diff --git a/locales/en/common.json b/locales/en/common.json index 82debeecf..eb28da0e0 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "Rule name is required", "validation_empty_conditions": "At least one condition with a value is required", "validation_empty_actions": "At least one action is required", + "validation_header_name": "Header names may only contain the letters A-Z, digits, hyphens and underscores", "templates_section": "Start from template", "template_newsletters": "Move newsletters to folder", "template_receipts": "Auto-archive receipts", diff --git a/locales/es/common.json b/locales/es/common.json index 5e26437ee..7975902a4 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "El nombre de la regla es obligatorio", "validation_empty_conditions": "Se requiere al menos una condición con un valor", "validation_empty_actions": "Se requiere al menos una acción", + "validation_header_name": "Los nombres de encabezado solo pueden contener las letras A-Z, dígitos, guiones y guiones bajos", "templates_section": "Usar una plantilla", "template_newsletters": "Mover newsletters a carpeta", "template_receipts": "Archivar recibos automáticamente", diff --git a/locales/fa/common.json b/locales/fa/common.json index 23c697541..319ce0e18 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "نام قانون الزامی است", "validation_empty_conditions": "حداقل یک شرط با مقدار الزامی است", "validation_empty_actions": "حداقل یک عملیات الزامی است", + "validation_header_name": "نام هدر فقط می‌تواند شامل حروف A-Z، ارقام، خط تیره و زیرخط باشد", "templates_section": "شروع از قالب", "template_newsletters": "انتقال خبرنامه‌ها به پوشه", "template_receipts": "بایگانی خودکار رسیدها", diff --git a/locales/fr/common.json b/locales/fr/common.json index 1877b7f86..f1949970a 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Le nom de la règle est requis", "validation_empty_conditions": "Au moins une condition avec une valeur est requise", "validation_empty_actions": "Au moins une action est requise", + "validation_header_name": "Les noms d'en-tête ne peuvent contenir que les lettres A-Z, des chiffres, des traits d'union et des tirets bas", "templates_section": "Partir d'un modèle", "template_newsletters": "Déplacer les newsletters dans un dossier", "template_receipts": "Archiver les reçus automatiquement", diff --git a/locales/he/common.json b/locales/he/common.json index c3a39c9a4..43d2d7534 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2004,6 +2004,7 @@ "validation_empty_name": "נדרש שם כלל", "validation_empty_conditions": "נדרש לפחות תנאי אחד עם ערך", "validation_empty_actions": "נדרשת לפחות פעולה אחת", + "validation_header_name": "שמות כותרות יכולים להכיל רק את האותיות A-Z, ספרות, מקפים וקווים תחתונים", "templates_section": "התחל מהתבנית", "template_newsletters": "העבר ניוזלטרים לתיקיה", "template_receipts": "העבר קבלות לארכיון אוטומטי", diff --git a/locales/hu/common.json b/locales/hu/common.json index ff51c0063..42f626610 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2087,6 +2087,7 @@ "validation_empty_name": "A szabály neve kötelező", "validation_empty_conditions": "Legalább egy feltétel szükséges értékkel", "validation_empty_actions": "Legalább egy művelet szükséges", + "validation_header_name": "A fejlécnevek csak az A-Z betűket, számjegyeket, kötőjeleket és aláhúzásjeleket tartalmazhatják", "templates_section": "Kezdés sablonból", "template_newsletters": "Hírlevelek áthelyezése mappába", "template_receipts": "Nyugták automatikus archiválása", diff --git a/locales/it/common.json b/locales/it/common.json index 0e2fe1581..2a26c411b 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Il nome della regola è obbligatorio", "validation_empty_conditions": "È necessaria almeno una condizione con un valore", "validation_empty_actions": "È necessaria almeno un'azione", + "validation_header_name": "I nomi delle intestazioni possono contenere solo le lettere A-Z, cifre, trattini e trattini bassi", "templates_section": "Parti da un modello", "template_newsletters": "Sposta newsletter in cartella", "template_receipts": "Archivia ricevute automaticamente", diff --git a/locales/ja/common.json b/locales/ja/common.json index 02e9a78f1..fc673867d 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "ルール名は必須です", "validation_empty_conditions": "値を持つ条件が少なくとも1つ必要です", "validation_empty_actions": "少なくとも1つのアクションが必要です", + "validation_header_name": "ヘッダー名に使用できるのは英字(A-Z)、数字、ハイフン、アンダースコアのみです", "templates_section": "テンプレートから作成", "template_newsletters": "ニュースレターをフォルダに移動", "template_receipts": "領収書を自動アーカイブ", diff --git a/locales/ko/common.json b/locales/ko/common.json index 6e5c3a6a1..bd04819e9 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "규칙 이름을 입력해 주세요", "validation_empty_conditions": "값을 포함한 조건이 하나 이상 필요해요", "validation_empty_actions": "동작이 하나 이상 필요해요", + "validation_header_name": "헤더 이름에는 영문자(A-Z), 숫자, 하이픈, 밑줄만 쓸 수 있어요", "templates_section": "템플릿으로 시작하기", "template_newsletters": "뉴스레터를 특정 폴더로 이동", "template_receipts": "영수증 자동 보관", diff --git a/locales/lv/common.json b/locales/lv/common.json index e83e5a77b..1e662e077 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Nepieciešams noteikuma nosaukums", "validation_empty_conditions": "Nepieciešams vismaz viens nosacījums ar vērtību", "validation_empty_actions": "Nepieciešama vismaz viena darbība", + "validation_header_name": "Galvenes nosaukumā drīkst būt tikai burti A-Z, cipari, defises un pasvītras", "templates_section": "Sākt no veidnes", "template_newsletters": "Pārvietot ziņu izdevumus uz mapi", "template_receipts": "Automātiski arhivēt rēķinus", diff --git a/locales/mn/common.json b/locales/mn/common.json index 1b98b3d59..c9bf36fbe 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "Дүрмийн нэр шаардлагатай", "validation_empty_conditions": "Дор хаяж нэг утга бүхий нөхцөл шаардлагатай", "validation_empty_actions": "Дор хаяж нэг үйлдэл хийх шаардлагатай", + "validation_header_name": "Толгойн нэрэнд зөвхөн A-Z үсэг, тоо, зураас, доогуур зураас ашиглаж болно", "templates_section": "Загвар", "template_newsletters": "Мэдээллийн хуудсыг хавтас руу зөөх", "template_receipts": "Автоматаар архивлах баримт", diff --git a/locales/nb/common.json b/locales/nb/common.json index b2247718e..cb3d22b54 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "Regelnavn må fylles ut", "validation_empty_conditions": "Minst ett vilkår må ha en verdi", "validation_empty_actions": "Du må legge til minst én handling", + "validation_header_name": "Navn på meldingshoder kan bare inneholde bokstavene A-Z, sifre, bindestreker og understreker", "templates_section": "Start med en mal", "template_newsletters": "Flytt nyhetsbrev til mappe", "template_receipts": "Arkiver kvitteringer automatisk", diff --git a/locales/nl/common.json b/locales/nl/common.json index 4ed80c08a..7d1f53300 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Regelnaam is verplicht", "validation_empty_conditions": "Minstens één voorwaarde met een waarde is vereist", "validation_empty_actions": "Minstens één actie is vereist", + "validation_header_name": "Headernamen mogen alleen de letters A-Z, cijfers, koppeltekens en underscores bevatten", "templates_section": "Starten vanuit sjabloon", "template_newsletters": "Nieuwsbrieven naar map verplaatsen", "template_receipts": "Bonnen automatisch archiveren", diff --git a/locales/pl/common.json b/locales/pl/common.json index 8d3aa1e77..20e08d4bc 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Nazwa reguły jest wymagana", "validation_empty_conditions": "Wymagany jest co najmniej jeden warunek z wartością", "validation_empty_actions": "Wymagana jest co najmniej jedna akcja", + "validation_header_name": "Nazwy nagłówków mogą zawierać tylko litery A-Z, cyfry, łączniki i podkreślenia", "templates_section": "Rozpocznij od szablonu", "template_newsletters": "Przenieś newslettery do folderu", "template_receipts": "Automatycznie archiwizuj rachunki", diff --git a/locales/pt/common.json b/locales/pt/common.json index 52c70e46f..684f5adb5 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "O nome da regra é obrigatório", "validation_empty_conditions": "É necessária pelo menos uma condição com um valor", "validation_empty_actions": "É necessária pelo menos uma ação", + "validation_header_name": "Os nomes de cabeçalho só podem conter as letras A-Z, algarismos, hífenes e sublinhados", "templates_section": "Começar com modelo", "template_newsletters": "Mover newsletters para pasta", "template_receipts": "Arquivar recibos automaticamente", diff --git a/locales/ro/common.json b/locales/ro/common.json index 6d6ff276f..a82fa40d2 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "Numele regulii este obligatoriu", "validation_empty_conditions": "Este necesară cel puțin o condiție cu o valoare", "validation_empty_actions": "Este necesară cel puțin o acțiune", + "validation_header_name": "Numele antetelor pot conține doar literele A-Z, cifre, cratime și liniuțe de subliniere", "templates_section": "Începeți de la șablon", "template_newsletters": "Mutați buletinele informative în dosar", "template_receipts": "Arhivarea automată a confirmărilor de primire", diff --git a/locales/ru/common.json b/locales/ru/common.json index 6ae734cc3..951187308 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Требуется название правила", "validation_empty_conditions": "Требуется хотя бы одно условие со значением", "validation_empty_actions": "Требуется хотя бы одно действие", + "validation_header_name": "Имя заголовка может содержать только буквы A-Z, цифры, дефисы и подчёркивания", "templates_section": "Начать с шаблона", "template_newsletters": "Перемещать рассылки в папку", "template_receipts": "Автоархивировать квитанции", diff --git a/locales/sk/common.json b/locales/sk/common.json index 045396c7b..12d4a13a5 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "Názov pravidla je povinný", "validation_empty_conditions": "Je potrebná aspoň jedna podmienka s hodnotou", "validation_empty_actions": "Je potrebná aspoň jedna akcia", + "validation_header_name": "Názvy hlavičiek môžu obsahovať iba písmená A-Z, číslice, spojovníky a podčiarkovníky", "templates_section": "Začať zo šablóny", "template_newsletters": "Presunúť newslettery do priečinka", "template_receipts": "Automaticky archivovať účtenky", diff --git a/locales/tr/common.json b/locales/tr/common.json index 4bb07c8f0..ebeac6f54 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Kural adı gerekli", "validation_empty_conditions": "En az bir değeri olan koşul gerekli", "validation_empty_actions": "En az bir işlem gerekli", + "validation_header_name": "Başlık adları yalnızca A-Z harflerini, rakamları, kısa çizgileri ve alt çizgileri içerebilir", "templates_section": "Şablondan başla", "template_newsletters": "Bültenleri klasöre taşı", "template_receipts": "Makbuzları otomatik arşivle", diff --git a/locales/uk/common.json b/locales/uk/common.json index e1be69e19..4347ddd94 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "Потрібно вказати назву правила", "validation_empty_conditions": "Потрібна принаймні одна умова зі значенням", "validation_empty_actions": "Необхідна принаймні одна дія", + "validation_header_name": "Назва заголовка може містити лише літери A-Z, цифри, дефіси та підкреслення", "templates_section": "Почніть із шаблону", "template_newsletters": "Перемістити інформаційні бюлетені в папку", "template_receipts": "Автоматичне архівування квитанцій", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index bd2e3cea7..de4511569 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -2091,6 +2091,7 @@ "validation_empty_name": "規則名稱為必填欄位", "validation_empty_conditions": "至少需要一個具有值的條件", "validation_empty_actions": "至少需要一個動作", + "validation_header_name": "標頭名稱只能包含字母 A-Z、數字、連字號和底線", "templates_section": "從範本開始", "template_newsletters": "將訂閱郵件移至信件匣", "template_receipts": "自動封存收據", diff --git a/locales/zh/common.json b/locales/zh/common.json index 7417206e7..60e4f7ab4 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2084,6 +2084,7 @@ "validation_empty_name": "规则名称为必填项", "validation_empty_conditions": "至少需要一个具有值的条件", "validation_empty_actions": "至少需要执行一项操作", + "validation_header_name": "标头名称只能包含字母 A-Z、数字、连字符和下划线", "templates_section": "从模板开始", "template_newsletters": "将订阅邮件移至文件夹", "template_receipts": "自动归档收据",