Files
SRCmail/lib/sieve/parser.ts
T
Matthieu MALVACHEandMatthieu MALVACHE 73612313c0 feat: add email filters with JMAP Sieve Scripts (RFC 9661)
Server-side email filtering with visual rule builder and raw Sieve editor.
Conditions (From/To/Subject/Size/Body), actions (Move/Forward/Star/Discard),
auto-save with rollback, drag-and-drop reorder, opaque script reset,
accessibility focus traps, toast validation, and 8-language i18n support.
2026-02-16 23:09:18 +01:00

69 lines
2.1 KiB
TypeScript

import type { FilterRule, FilterMetadata } from '@/lib/jmap/sieve-types';
import { debug } from '@/lib/debug';
export interface ParseResult {
rules: FilterRule[];
isOpaque: boolean;
}
const OPAQUE: ParseResult = { rules: [], isOpaque: true };
const METADATA_BEGIN = '/* @metadata:begin';
const METADATA_END = '@metadata:end */';
function isValidCondition(c: unknown): boolean {
if (!c || typeof c !== 'object') return false;
const cond = c as Record<string, unknown>;
return typeof cond.field === 'string' && typeof cond.comparator === 'string' && typeof cond.value === 'string';
}
function isValidAction(a: unknown): boolean {
if (!a || typeof a !== 'object') return false;
const act = a as Record<string, unknown>;
return typeof act.type === 'string';
}
function isValidRule(rule: unknown): rule is FilterRule {
if (!rule || typeof rule !== 'object') return false;
const r = rule as Record<string, unknown>;
if (
typeof r.id !== 'string' ||
typeof r.name !== 'string' ||
typeof r.enabled !== 'boolean' ||
(r.matchType !== 'all' && r.matchType !== 'any') ||
!Array.isArray(r.conditions) ||
!Array.isArray(r.actions) ||
typeof r.stopProcessing !== 'boolean'
) return false;
return r.conditions.every(isValidCondition) && r.actions.every(isValidAction);
}
export function parseScript(content: string): ParseResult {
const beginIdx = content.indexOf(METADATA_BEGIN);
if (beginIdx === -1) return OPAQUE;
const endIdx = content.indexOf(METADATA_END, beginIdx);
if (endIdx === -1) return OPAQUE;
const jsonStart = beginIdx + METADATA_BEGIN.length;
const jsonStr = content.slice(jsonStart, endIdx).trim();
let metadata: FilterMetadata;
try {
metadata = JSON.parse(jsonStr);
} catch (e) {
debug.warn('Failed to parse Sieve metadata JSON:', e);
return OPAQUE;
}
if (!metadata || metadata.version !== 1) return OPAQUE;
if (!Array.isArray(metadata.rules)) return OPAQUE;
for (const rule of metadata.rules) {
if (!isValidRule(rule)) return OPAQUE;
}
return { rules: metadata.rules, isOpaque: false };
}