7 } from '@proton/components/containers/filters/interfaces';
10 ConditionComparatorInvertedMap,
12 } from '@proton/components/containers/filters/interfaces';
14 import { TEST_NODES, V1, V2 } from '../constants';
15 import { escapeCharacters, escapeVariables, unique } from '../helpers';
16 import type { BuildFileIntoType, EscapeVariableType, SIEVE_VERSION, SieveCondition, ValueTypePair } from '../interface';
17 import { MATCH_KEYS, OPERATOR_KEYS } from '../interface';
20 * Builds a vacation action.
22 const buildVacationAction = (message: string | ValueTypePair, version: SIEVE_VERSION) => {
25 Args: { MIMEType: 'text/html' },
26 Type: version === V1 ? 'Vacation\\Vacation' : 'Vacation',
31 * Builds a setFlag action, for read or starred.
33 const buildSetFlagThen = (read: boolean, starred: boolean) => {
41 flags.push('\\Flagged');
51 * Builds a require node.
53 const buildSieveRequire = (requires: string[], mandatory: string[] = ['fileinto', 'imap4flags']) => {
55 List: unique([...mandatory, ...requires]),
61 * Builds a file into action.
63 const buildFileIntoAction = (name: string | ValueTypePair) => {
71 * Build the comment node from a comparator.
73 const buildComparatorComment = (comparators: string[], type: FilterStatement) => {
74 const commentArray = ['/**'];
76 if (OPERATOR_KEYS[type] === OPERATOR_KEYS.all) {
77 commentArray.push(' @type and');
78 } else if (OPERATOR_KEYS[type] === OPERATOR_KEYS.any) {
79 commentArray.push(' @type or');
82 commentArray.push(...comparators.map((comparator) => ' @comparator ' + comparator));
84 commentArray.push('/');
86 Text: commentArray.join('\r\n *'),
92 * Builds an address test.
94 const buildAddressTest = (headers: string[], keys: EscapeVariableType[], match: string) => {
102 Type: 'UnicodeCaseMap',
112 * Build match and values from comparator and condition.
114 const buildMatchAndValues = (comparator: ConditionComparator, condition: { Values: string[] }) => {
115 // starts and ends does not exists in sieve. Replacing it to match.
116 const values = condition.Values.map((value) => {
117 const escaped = escapeCharacters(value);
118 if (comparator === ConditionComparator.STARTS) {
119 return `${escaped}*`;
122 if (comparator === ConditionComparator.ENDS) {
123 return `*${escaped}`;
129 let val = MATCH_KEYS.default;
130 if (comparator === ConditionComparator.STARTS || comparator === ConditionComparator.ENDS) {
131 val = MATCH_KEYS.matches;
133 val = MATCH_KEYS[comparator];
138 match: val ?? MATCH_KEYS.default ?? 'Defaults',
143 * Builds a simple test.
145 const buildSimpleHeaderTest = (headers: string[], keys: EscapeVariableType[], match: string) => {
150 Type: match, // The value can be removed if needed, it's backend compatible.
153 Type: 'UnicodeCaseMap',
160 * Prepare comparator.
162 const prepareComparator = (comparator: ConditionComparator) => {
163 const mappedCondition = ConditionComparatorInvertedMap.get(comparator);
164 if (mappedCondition) {
167 comparator: mappedCondition,
178 * Negates a given test.
180 const buildTestNegate = <T>(test: T) => {
188 * Prepare the comment.
190 const prepareComment = (comparator: string, type: string, negate: boolean) => {
191 const negation = negate ? '!' : '';
192 if (type === 'attachments') {
193 return `${negation}default`;
195 return `${negation}${comparator}`;
199 * Builds mark blocks.
201 export const buildMark = ({ Read: read, Starred: starred }: { Read: boolean; Starred: boolean }) => {
202 if (!read && !starred) {
203 return { blocks: [] };
206 return { blocks: [buildSetFlagThen(read, starred), { Type: 'Keep' }] };
209 export const buildRedirects = (redirects?: FilterRedirect[]) => {
211 return { blocks: [] };
214 blocks: redirects.map((redirect) => ({
215 Address: redirect.Address,
222 * Build vacation blocks.
224 export const buildVacation = (vacation: string | null | undefined, version: SIEVE_VERSION) => {
226 return { blocks: [] };
229 const message = escapeVariables(vacation);
231 blocks: [buildVacationAction(message, version)],
232 dollarNeeded: typeof message === 'object',
239 export const buildBasicTree = (
242 comparators: string[];
243 type: FilterStatement;
246 dollarNeeded?: boolean;
248 version: SIEVE_VERSION
250 const treeStructure = [];
251 if (version === V2) {
254 ['include', 'environment', 'variables', 'relational', 'comparator-i;ascii-numeric', 'spamtest'],
260 treeStructure.push(buildSieveRequire(parameters.requires));
262 if (version === V2) {
263 treeStructure.push(...TEST_NODES.spamtest);
265 if (parameters.dollarNeeded) {
266 treeStructure.push(...TEST_NODES.dollar);
269 treeStructure.push(buildComparatorComment(parameters.comparators, parameters.type));
274 Tests: parameters.tests,
275 Type: OPERATOR_KEYS[parameters.type],
277 Then: parameters.thens,
280 return treeStructure;
284 * Validates the received simple representation.
286 export const validateSimpleRepresentation = (simple: SimpleObject) => {
288 if (!(simple.Operator instanceof Object && Array.isArray(simple.Conditions) && simple.Actions instanceof Object)) {
289 throw new Error('Invalid simple data types');
292 if (!simple.Operator.label || !simple.Operator.value) {
293 throw new Error('Invalid simple operator');
296 simple.Conditions.forEach((condition) => {
297 if (!condition.Values) {
298 throw new Error('Invalid simple conditions');
301 const { comparator } = prepareComparator(condition.Comparator.value);
302 if (!MATCH_KEYS[comparator] || MATCH_KEYS[comparator] === MATCH_KEYS.default) {
303 throw new Error('Unrecognized simple condition: ' + condition.Comparator.value);
308 !simple.Actions.FileInto ||
309 !Array.isArray(simple.Actions.FileInto) ||
310 !simple.Actions.Mark ||
311 simple.Actions.Mark.Read === undefined ||
312 simple.Actions.Mark.Starred === undefined
314 throw new Error('Invalid simple actions');
321 * Builds fileInto blocks.
323 export const buildFileInto = (actions: FilterActions['FileInto']) => {
324 const initialObject: {
325 dollarNeeded: boolean;
326 blocks: BuildFileIntoType[];
332 actions.forEach((action) => {
333 const formattedAction = escapeVariables(action);
335 if (typeof formattedAction === 'object') {
336 initialObject.dollarNeeded = true;
339 initialObject.blocks.push(buildFileIntoAction(formattedAction));
342 return initialObject;
345 //A bit hard to migrate
347 * Builds condition block.
349 export const buildCondition = (conditions: FilterCondition[]): SieveCondition => {
350 const initialObject: SieveCondition = {
356 conditions.forEach((condition) => {
357 const { comparator, negate } = prepareComparator(condition.Comparator.value);
358 const { match, values: matchValues } = buildMatchAndValues(comparator, condition);
360 const values = matchValues.map(escapeVariables);
361 const conditionMap = {
362 sender: () => buildAddressTest(['From'], values, match),
363 recipient: () => buildAddressTest(['To', 'Cc', 'Bcc'], values, match),
364 subject: () => buildSimpleHeaderTest(['Subject'], values, match),
365 attachments: () => TEST_NODES.attachment[0],
367 const conditionMapping = new Map([
368 [ConditionType.SENDER, conditionMap.sender],
369 [ConditionType.RECIPIENT, conditionMap.recipient],
370 [ConditionType.SUBJECT, conditionMap.subject],
371 [ConditionType.ATTACHMENTS, conditionMap.attachments],
374 const mappedCondition = conditionMapping.get(condition.Type.value);
375 if (mappedCondition) {
376 const test = mappedCondition();
377 initialObject.tests.push(negate ? buildTestNegate(test) : test);
380 initialObject.dollarNeeded = values.some((value) => typeof value !== 'string');
381 initialObject.comparators.push(prepareComment(comparator, condition.Type.value, negate));
384 return initialObject;