mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-01 05:29:20 +03:00
Added Sieve parser code in master
This commit is contained in:
parent
e90e8a55ce
commit
f4cd25f8ad
21 changed files with 2126 additions and 8 deletions
367
dev/Sieve/Parser.js
Normal file
367
dev/Sieve/Parser.js
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
/**
|
||||
* https://tools.ietf.org/html/rfc5228#section-8
|
||||
*/
|
||||
|
||||
import {
|
||||
BRACKET_COMMENT,
|
||||
HASH_COMMENT,
|
||||
IDENTIFIER,
|
||||
MULTI_LINE,
|
||||
NUMBER,
|
||||
QUOTED_STRING,
|
||||
STRING_LIST,
|
||||
TAG
|
||||
} from 'Sieve/RegEx';
|
||||
|
||||
import {
|
||||
GrammarBracketComment,
|
||||
GrammarCommand,
|
||||
GrammarHashComment,
|
||||
GrammarMultiLine,
|
||||
GrammarNumber,
|
||||
GrammarQuotedString,
|
||||
GrammarStringList,
|
||||
GrammarTest,
|
||||
GrammarTestList
|
||||
} from 'Sieve/Grammar';
|
||||
|
||||
import {
|
||||
ConditionalCommand,
|
||||
DiscardCommand,
|
||||
ElsIfCommand,
|
||||
ElseCommand,
|
||||
FileIntoCommand,
|
||||
IfCommand,
|
||||
KeepCommand,
|
||||
RedirectCommand,
|
||||
RequireCommand,
|
||||
StopCommand
|
||||
} from 'Sieve/Commands';
|
||||
|
||||
import {
|
||||
AddressCommand,
|
||||
AllOfCommand,
|
||||
AnyOfCommand,
|
||||
EnvelopeCommand,
|
||||
ExistsCommand,
|
||||
FalseCommand,
|
||||
HeaderCommand,
|
||||
NotCommand,
|
||||
SizeCommand,
|
||||
TrueCommand
|
||||
} from 'Sieve/Tests';
|
||||
|
||||
import {
|
||||
VacationCommand
|
||||
} from 'Sieve/Extensions/rfc5230';
|
||||
|
||||
import {
|
||||
SetFlagCommand,
|
||||
AddFlagCommand,
|
||||
RemoveFlagCommand,
|
||||
HasFlagCommand
|
||||
} from 'Sieve/Extensions/rfc5232';
|
||||
|
||||
import {
|
||||
ErejectCommand,
|
||||
RejectCommand
|
||||
} from 'Sieve/Extensions/rfc5429';
|
||||
|
||||
import {
|
||||
IncludeCommand,
|
||||
ReturnCommand
|
||||
} from 'Sieve/Extensions/rfc6609';
|
||||
|
||||
const
|
||||
Commands = {
|
||||
// Control commands
|
||||
if: IfCommand,
|
||||
elsif: ElsIfCommand,
|
||||
else: ElseCommand,
|
||||
conditional: ConditionalCommand,
|
||||
require: RequireCommand,
|
||||
stop: StopCommand,
|
||||
// Action commands
|
||||
discard: DiscardCommand,
|
||||
fileinto: FileIntoCommand,
|
||||
keep: KeepCommand,
|
||||
redirect: RedirectCommand,
|
||||
// Test commands
|
||||
address: AddressCommand,
|
||||
allof: AllOfCommand,
|
||||
anyof: AnyOfCommand,
|
||||
envelope: EnvelopeCommand,
|
||||
exists: ExistsCommand,
|
||||
false: FalseCommand,
|
||||
header: HeaderCommand,
|
||||
not: NotCommand,
|
||||
size: SizeCommand,
|
||||
true: TrueCommand,
|
||||
// rfc5230
|
||||
vacation: VacationCommand,
|
||||
// rfc5232
|
||||
setflag: SetFlagCommand,
|
||||
addflag: AddFlagCommand,
|
||||
removeflag: RemoveFlagCommand,
|
||||
hasflag: HasFlagCommand,
|
||||
// rfc5429
|
||||
ereject: ErejectCommand,
|
||||
reject: RejectCommand,
|
||||
// rfc6609
|
||||
include: IncludeCommand,
|
||||
return: ReturnCommand
|
||||
},
|
||||
|
||||
T_UNKNOWN = 0,
|
||||
T_STRING_LIST = 1,
|
||||
T_QUOTED_STRING = 2,
|
||||
T_MULTILINE_STRING = 3,
|
||||
T_HASH_COMMENT = 4,
|
||||
T_BRACKET_COMMENT = 5,
|
||||
T_BLOCK_START = 6,
|
||||
T_BLOCK_END = 7,
|
||||
T_LEFT_PARENTHESIS = 8,
|
||||
T_RIGHT_PARENTHESIS = 9,
|
||||
T_COMMA = 10,
|
||||
T_SEMICOLON = 11,
|
||||
T_TAG = 12,
|
||||
T_IDENTIFIER = 13,
|
||||
T_NUMBER = 14,
|
||||
T_WHITESPACE = 15,
|
||||
|
||||
TokensRegEx = '(' + [
|
||||
/* T_STRING_LIST */ STRING_LIST,
|
||||
/* T_QUOTED_STRING */ QUOTED_STRING,
|
||||
/* T_MULTILINE_STRING */ MULTI_LINE,
|
||||
/* T_HASH_COMMENT */ HASH_COMMENT,
|
||||
/* T_BRACKET_COMMENT */ BRACKET_COMMENT,
|
||||
/* T_BLOCK_START */ '\\{',
|
||||
/* T_BLOCK_END */ '\\}',
|
||||
/* T_LEFT_PARENTHESIS */ '\\(', // anyof / allof
|
||||
/* T_RIGHT_PARENTHESIS */ '\\)', // anyof / allof
|
||||
/* T_COMMA */ ',',
|
||||
/* T_SEMICOLON */ ';',
|
||||
/* T_TAG */ TAG,
|
||||
/* T_IDENTIFIER */ IDENTIFIER,
|
||||
/* T_NUMBER */ NUMBER,
|
||||
/* T_WHITESPACE */ '(?: |\\r\\n|\\t)+',
|
||||
/* T_UNKNOWN */ '[^ \\r\\n\\t]+'
|
||||
].join(')|(') + ')';
|
||||
|
||||
export const parseScript = (script, name = 'script.sieve') => {
|
||||
script = script.replace(/\r?\n/g, '\r\n');
|
||||
|
||||
let match,
|
||||
line = 1,
|
||||
tree = [],
|
||||
|
||||
// Create one regex to find the tokens
|
||||
// Use exec() to forward since lastIndex
|
||||
regex = RegExp(TokensRegEx, 'gm'),
|
||||
|
||||
levels = [],
|
||||
command = null,
|
||||
requires = [],
|
||||
args = [];
|
||||
|
||||
const
|
||||
error = message => {
|
||||
// throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, name, line)
|
||||
throw new SyntaxError(message + ' on line ' + line
|
||||
+ ' around: ' + script.substr(regex.lastIndex - 10, 20).replace(/\r\n/g, '\\r\\n'), name, line)
|
||||
},
|
||||
pushArg = arg => {
|
||||
command || error('Argument not part of command');
|
||||
let prev_arg = args[args.length-1];
|
||||
if (':is' === arg || ':contains' === arg || ':matches' === arg) {
|
||||
command.match_type = arg;
|
||||
} else if (':value' === prev_arg || ':count' === prev_arg) {
|
||||
// Sieve relational [RFC5231] match types
|
||||
/^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
|
||||
command.match_type = prev_arg + ' ' + arg;
|
||||
--args.length;
|
||||
// requires.push('relational');
|
||||
} else if (':comparator' === prev_arg) {
|
||||
command.comparator = arg;
|
||||
--args.length;
|
||||
} else {
|
||||
args.push(arg);
|
||||
}
|
||||
},
|
||||
pushArgs = () => {
|
||||
if (args.length) {
|
||||
command && command.pushArguments(args);
|
||||
args = [];
|
||||
}
|
||||
};
|
||||
|
||||
levels.last = () => levels[levels.length - 1];
|
||||
|
||||
while ((match = regex.exec(script))) {
|
||||
// the last element in match will contain the matched value and the key will be the type
|
||||
let type = match.findIndex((v,i) => 0 < i && undefined !== v),
|
||||
value = match[type];
|
||||
|
||||
// create the part
|
||||
switch (type)
|
||||
{
|
||||
case T_IDENTIFIER: {
|
||||
pushArgs();
|
||||
value = value.toLowerCase();
|
||||
let new_command;
|
||||
if ('if' === value) {
|
||||
new_command = new ConditionalCommand(value);
|
||||
} else if ('elsif' === value || 'else' === value) {
|
||||
// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
|
||||
new_command = new ConditionalCommand(value);
|
||||
} else if (Commands[value]) {
|
||||
if ('allof' === value || 'anyof' === value) {
|
||||
// (command instanceof ConditionalCommand || command instanceof NotCommand) || error('Test-list not in conditional');
|
||||
}
|
||||
new_command = new Commands[value]();
|
||||
} else {
|
||||
console.error('Unknown command: ' + value);
|
||||
if (command && (
|
||||
command instanceof ConditionalCommand
|
||||
|| command instanceof NotCommand
|
||||
|| command.tests instanceof GrammarTestList)) {
|
||||
new_command = new GrammarTest(value);
|
||||
} else {
|
||||
new_command = new GrammarCommand(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (new_command instanceof GrammarTest) {
|
||||
if (command instanceof ConditionalCommand || command instanceof NotCommand) {
|
||||
// if/elsif/else new_command
|
||||
// not new_command
|
||||
command.test = new_command;
|
||||
} else if (command.tests instanceof GrammarTestList) {
|
||||
// allof/anyof .tests[] new_command
|
||||
command.tests.push(new_command);
|
||||
} else {
|
||||
error('Test "' + value + '" not allowed in "' + command.identifier + '" command');
|
||||
}
|
||||
} else if (command) {
|
||||
if (command.commands) {
|
||||
command.commands.push(new_command);
|
||||
} else {
|
||||
error('commands not allowed in "' + command.identifier + '" command');
|
||||
}
|
||||
} else {
|
||||
tree.push(new_command);
|
||||
}
|
||||
levels.push(new_command);
|
||||
command = new_command;
|
||||
if (command.require) {
|
||||
(Array.isArray(command.require) ? command.require : [command.require])
|
||||
.forEach(string => requires.push(string));
|
||||
}
|
||||
if (command.comparator) {
|
||||
requires.push('comparator-' + command.comparator);
|
||||
}
|
||||
break; }
|
||||
|
||||
// Arguments
|
||||
case T_TAG:
|
||||
pushArg(value.toLowerCase());
|
||||
break;
|
||||
case T_STRING_LIST:
|
||||
pushArg(GrammarStringList.fromString(value));
|
||||
break;
|
||||
case T_MULTILINE_STRING:
|
||||
pushArg(GrammarMultiLine.fromString(value));
|
||||
break;
|
||||
case T_QUOTED_STRING:
|
||||
pushArg(new GrammarQuotedString(value.substr(1,value.length-2)));
|
||||
break;
|
||||
case T_NUMBER:
|
||||
pushArg(new GrammarNumber(value));
|
||||
break;
|
||||
|
||||
// Comments
|
||||
case T_BRACKET_COMMENT:
|
||||
case T_HASH_COMMENT: {
|
||||
let obj = (T_HASH_COMMENT == type)
|
||||
? new GrammarHashComment(value.substr(1).trim())
|
||||
: new GrammarBracketComment(value.substr(2, value.length-4));
|
||||
if (command) {
|
||||
if (!command.comments) {
|
||||
command.comments = [];
|
||||
}
|
||||
(command.commands || command.comments).push(obj);
|
||||
} else {
|
||||
tree.push(obj);
|
||||
}
|
||||
break; }
|
||||
|
||||
case T_WHITESPACE:
|
||||
// (command ? command.commands : tree).push(value.trim());
|
||||
command || tree.push(value.trim());
|
||||
break;
|
||||
|
||||
// Command end
|
||||
case T_SEMICOLON:
|
||||
command || error('Semicolon not at end of command');
|
||||
pushArgs();
|
||||
if (command instanceof RequireCommand) {
|
||||
command.capabilities.forEach(string => requires.push(string.value));
|
||||
}
|
||||
levels.pop();
|
||||
command = levels.last();
|
||||
break;
|
||||
|
||||
// Command block
|
||||
case T_BLOCK_START:
|
||||
pushArgs();
|
||||
// https://tools.ietf.org/html/rfc5228#section-2.9
|
||||
// Action commands do not take tests or blocks
|
||||
while (command && !(command instanceof ConditionalCommand)) {
|
||||
levels.pop();
|
||||
command = levels.last();
|
||||
}
|
||||
command || error('Block start not part of control command');
|
||||
break;
|
||||
case T_BLOCK_END:
|
||||
(command instanceof ConditionalCommand) || error('Block end has no matching block start');
|
||||
levels.pop();
|
||||
// prev_command = command;
|
||||
command = levels.last();
|
||||
break;
|
||||
|
||||
// anyof / allof ( ... , ... )
|
||||
case T_LEFT_PARENTHESIS:
|
||||
pushArgs();
|
||||
while (command && !(command.tests instanceof GrammarTestList)) {
|
||||
levels.pop();
|
||||
command = levels.last();
|
||||
}
|
||||
command || error('Test start not part of anyof/allof test');
|
||||
break;
|
||||
case T_RIGHT_PARENTHESIS:
|
||||
pushArgs();
|
||||
levels.pop();
|
||||
command = levels.last();
|
||||
(command.tests instanceof GrammarTestList) || error('Test end not part of test-list');
|
||||
break;
|
||||
case T_COMMA:
|
||||
pushArgs();
|
||||
// Must be inside PARENTHESIS aka test-list
|
||||
while (command && !(command.tests instanceof GrammarTestList)) {
|
||||
levels.pop();
|
||||
command = levels.last();
|
||||
}
|
||||
command || error('Comma not part of test-list');
|
||||
break;
|
||||
|
||||
case T_UNKNOWN:
|
||||
error('Invalid token ' + value);
|
||||
}
|
||||
|
||||
// Set current script position
|
||||
line += (value.split('\n').length - 1); // (value.match(/\n/g) || []).length;
|
||||
}
|
||||
|
||||
tree.requires = requires;
|
||||
return tree;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue