Remove client-side isLoggedIn value
[ProtonMail-WebClient.git] / packages / sieve / src / toSieve / toSieveTree.helpers.ts
blob3178880645d2953cd1e7e4b59be0c3d96b05df9c
1 import type {
2     FilterActions,
3     FilterCondition,
4     FilterRedirect,
5     FilterStatement,
6     SimpleObject,
7 } from '@proton/components/containers/filters/interfaces';
8 import {
9     ConditionComparator,
10     ConditionComparatorInvertedMap,
11     ConditionType,
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';
19 /**
20  * Builds a vacation action.
21  */
22 const buildVacationAction = (message: string | ValueTypePair, version: SIEVE_VERSION) => {
23     return {
24         Message: message,
25         Args: { MIMEType: 'text/html' },
26         Type: version === V1 ? 'Vacation\\Vacation' : 'Vacation',
27     };
30 /**
31  * Builds a setFlag action, for read or starred.
32  */
33 const buildSetFlagThen = (read: boolean, starred: boolean) => {
34     const flags = [];
36     if (read) {
37         flags.push('\\Seen');
38     }
40     if (starred) {
41         flags.push('\\Flagged');
42     }
44     return {
45         Flags: flags,
46         Type: 'AddFlag',
47     };
50 /**
51  * Builds a require node.
52  */
53 const buildSieveRequire = (requires: string[], mandatory: string[] = ['fileinto', 'imap4flags']) => {
54     return {
55         List: unique([...mandatory, ...requires]),
56         Type: 'Require',
57     };
60 /**
61  * Builds a file into action.
62  */
63 const buildFileIntoAction = (name: string | ValueTypePair) => {
64     return {
65         Name: name,
66         Type: 'FileInto',
67     };
70 /**
71  * Build the comment node from a comparator.
72  */
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');
80     }
82     commentArray.push(...comparators.map((comparator) => ' @comparator ' + comparator));
84     commentArray.push('/');
85     return {
86         Text: commentArray.join('\r\n *'),
87         Type: 'Comment',
88     };
91 /**
92  * Builds an address test.
93  */
94 const buildAddressTest = (headers: string[], keys: EscapeVariableType[], match: string) => {
95     return {
96         Headers: headers,
97         Keys: keys,
98         Match: {
99             Type: match,
100         },
101         Format: {
102             Type: 'UnicodeCaseMap',
103         },
104         Type: 'Address',
105         AddressPart: {
106             Type: 'All',
107         },
108     };
112  * Build match and values from comparator and condition.
113  */
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}*`;
120         }
122         if (comparator === ConditionComparator.ENDS) {
123             return `*${escaped}`;
124         }
126         return value;
127     });
129     let val = MATCH_KEYS.default;
130     if (comparator === ConditionComparator.STARTS || comparator === ConditionComparator.ENDS) {
131         val = MATCH_KEYS.matches;
132     } else {
133         val = MATCH_KEYS[comparator];
134     }
136     return {
137         values,
138         match: val ?? MATCH_KEYS.default ?? 'Defaults',
139     };
143  * Builds a simple test.
144  */
145 const buildSimpleHeaderTest = (headers: string[], keys: EscapeVariableType[], match: string) => {
146     return {
147         Headers: headers,
148         Keys: keys,
149         Match: {
150             Type: match, // The value can be removed if needed, it's backend compatible.
151         },
152         Format: {
153             Type: 'UnicodeCaseMap',
154         },
155         Type: 'Header',
156     };
160  * Prepare comparator.
161  */
162 const prepareComparator = (comparator: ConditionComparator) => {
163     const mappedCondition = ConditionComparatorInvertedMap.get(comparator);
164     if (mappedCondition) {
165         return {
166             negate: true,
167             comparator: mappedCondition,
168         };
169     }
171     return {
172         negate: false,
173         comparator,
174     };
178  * Negates a given test.
179  */
180 const buildTestNegate = <T>(test: T) => {
181     return {
182         Test: test,
183         Type: 'Not',
184     };
188  * Prepare the comment.
189  */
190 const prepareComment = (comparator: string, type: string, negate: boolean) => {
191     const negation = negate ? '!' : '';
192     if (type === 'attachments') {
193         return `${negation}default`;
194     }
195     return `${negation}${comparator}`;
199  * Builds mark blocks.
200  */
201 export const buildMark = ({ Read: read, Starred: starred }: { Read: boolean; Starred: boolean }) => {
202     if (!read && !starred) {
203         return { blocks: [] };
204     }
206     return { blocks: [buildSetFlagThen(read, starred), { Type: 'Keep' }] };
209 export const buildRedirects = (redirects?: FilterRedirect[]) => {
210     if (!redirects) {
211         return { blocks: [] };
212     }
213     return {
214         blocks: redirects.map((redirect) => ({
215             Address: redirect.Address,
216             Type: 'Redirect',
217         })),
218     };
222  * Build vacation blocks.
223  */
224 export const buildVacation = (vacation: string | null | undefined, version: SIEVE_VERSION) => {
225     if (!vacation) {
226         return { blocks: [] };
227     }
229     const message = escapeVariables(vacation);
230     return {
231         blocks: [buildVacationAction(message, version)],
232         dollarNeeded: typeof message === 'object',
233     };
237  * Builds the tree.
238  */
239 export const buildBasicTree = (
240     parameters: {
241         requires: string[];
242         comparators: string[];
243         type: FilterStatement;
244         tests: any;
245         thens: any[];
246         dollarNeeded?: boolean;
247     },
248     version: SIEVE_VERSION
249 ) => {
250     const treeStructure = [];
251     if (version === V2) {
252         treeStructure.push(
253             buildSieveRequire(
254                 ['include', 'environment', 'variables', 'relational', 'comparator-i;ascii-numeric', 'spamtest'],
255                 []
256             )
257         );
258     }
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);
267         }
269         treeStructure.push(buildComparatorComment(parameters.comparators, parameters.type));
270     }
272     treeStructure.push({
273         If: {
274             Tests: parameters.tests,
275             Type: OPERATOR_KEYS[parameters.type],
276         },
277         Then: parameters.thens,
278         Type: 'If',
279     });
280     return treeStructure;
284  * Validates the received simple representation.
285  */
286 export const validateSimpleRepresentation = (simple: SimpleObject) => {
287     /* beware the not */
288     if (!(simple.Operator instanceof Object && Array.isArray(simple.Conditions) && simple.Actions instanceof Object)) {
289         throw new Error('Invalid simple data types');
290     }
292     if (!simple.Operator.label || !simple.Operator.value) {
293         throw new Error('Invalid simple operator');
294     }
296     simple.Conditions.forEach((condition) => {
297         if (!condition.Values) {
298             throw new Error('Invalid simple conditions');
299         }
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);
304         }
305     });
307     if (
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
313     ) {
314         throw new Error('Invalid simple actions');
315     }
317     return simple;
321  * Builds fileInto blocks.
322  */
323 export const buildFileInto = (actions: FilterActions['FileInto']) => {
324     const initialObject: {
325         dollarNeeded: boolean;
326         blocks: BuildFileIntoType[];
327     } = {
328         dollarNeeded: false,
329         blocks: [],
330     };
332     actions.forEach((action) => {
333         const formattedAction = escapeVariables(action);
335         if (typeof formattedAction === 'object') {
336             initialObject.dollarNeeded = true;
337         }
339         initialObject.blocks.push(buildFileIntoAction(formattedAction));
340     });
342     return initialObject;
345 //A bit hard to migrate
347  * Builds condition block.
348  */
349 export const buildCondition = (conditions: FilterCondition[]): SieveCondition => {
350     const initialObject: SieveCondition = {
351         tests: [],
352         comparators: [],
353         dollarNeeded: false,
354     };
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],
366         };
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],
372         ]);
374         const mappedCondition = conditionMapping.get(condition.Type.value);
375         if (mappedCondition) {
376             const test = mappedCondition();
377             initialObject.tests.push(negate ? buildTestNegate(test) : test);
378         }
380         initialObject.dollarNeeded = values.some((value) => typeof value !== 'string');
381         initialObject.comparators.push(prepareComment(comparator, condition.Type.value, negate));
382     });
384     return initialObject;