From b2e77f3f67eb19095fc66885ad591405cddc1dfe Mon Sep 17 00:00:00 2001 From: djmaze Date: Wed, 13 Jan 2021 21:23:19 +0100 Subject: [PATCH] Draf of Sieve parser/lexer for a new Sieve GUI --- dev/Sieve/Commands.js | 171 +++++++++++++++++ dev/Sieve/Extensions/Body.js | 28 +++ dev/Sieve/Extensions/Ereject.js | 30 +++ dev/Sieve/Extensions/Imap4flags.js | 17 ++ dev/Sieve/Extensions/Reject.js | 30 +++ dev/Sieve/Extensions/Vacation.js | 84 +++++++++ dev/Sieve/Grammar.js | 286 +++++++++++++++++++++++++++++ dev/Sieve/Parser.js | 230 +++++++++++++++++++++++ dev/Sieve/RegEx.js | 219 ++++++++++++++++++++++ dev/Sieve/Tests.js | 218 ++++++++++++++++++++++ dev/sieve.js | 15 ++ tasks/config.js | 12 ++ tasks/js.js | 14 +- 13 files changed, 1353 insertions(+), 1 deletion(-) create mode 100644 dev/Sieve/Commands.js create mode 100644 dev/Sieve/Extensions/Body.js create mode 100644 dev/Sieve/Extensions/Ereject.js create mode 100644 dev/Sieve/Extensions/Imap4flags.js create mode 100644 dev/Sieve/Extensions/Reject.js create mode 100644 dev/Sieve/Extensions/Vacation.js create mode 100644 dev/Sieve/Grammar.js create mode 100644 dev/Sieve/Parser.js create mode 100644 dev/Sieve/RegEx.js create mode 100644 dev/Sieve/Tests.js create mode 100644 dev/sieve.js diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js new file mode 100644 index 000000000..8555b33dc --- /dev/null +++ b/dev/Sieve/Commands.js @@ -0,0 +1,171 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-2.9 + */ + +(Sieve => { + +/** + * https://tools.ietf.org/html/rfc5228#section-3.1 + * Usage: + * if + * elsif + * else + */ +class Conditional +{ + constructor(identifier = 'if') + { + this.identifier = identifier; + this.test = null, + this.commands = []; + } + + toString() + { + return this.identifier + + ('else' !== this.identifier ? ' ' + this.test : '') + + " {\r\n\t" + this.commands.join(";\r\n\t") + "\r\n}"; + } +/* + public function pushArguments(array $args): void + { + print_r($args); + exit; + } +*/ +} + +/** + * https://tools.ietf.org/html/rfc5228#section-3.2 + */ +class Requires /* extends Array*/ +{ + constructor() + { + this.capabilities = new Sieve.Grammar.StringList(); + } + + toString() + { + return 'require ' + this.capabilities; + } + + pushArguments(args) + { + if (args[0] instanceof Sieve.Grammar.StringList) { + this.capabilities = args[0]; + } else if (args[0] instanceof Sieve.Grammar.QuotedString) { + this.capabilities.push(args[0]); + } + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-3.3 + */ +class Stop +{ + toString() + { + return 'stop'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.1 + */ +class FileInto +{ +// const REQUIRE = 'fileinto'; + + constructor() + { + // QuotedString / MultiLine + this._mailbox = new Sieve.Grammar.QuotedString(); + } + + toString() + { + return 'fileinto ' + this.mailbox; + } + + get mailbox() + { + return this._mailbox.value; + } + + set mailbox(value) + { + this._mailbox.value = value; + } + + pushArguments(args) + { + if (args[0] instanceof Sieve.Grammar.StringType) { + this._mailbox = args[0]; + } + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.2 + */ +class Redirect +{ + constructor() + { + // QuotedString / MultiLine + this._address = new Sieve.Grammar.QuotedString(); + } + + toString() + { + return 'redirect ' + this.address; + } + + get address() + { + return this._address.value; + } + + set address(value) + { + this._address.value = value; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.3 + */ +class Keep +{ + toString() + { + return 'keep'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.4 + */ +class Discard +{ + toString() + { + return 'discard'; + } +} + +Sieve.Commands = { + // Control commands + Conditional: Conditional, + Requires: Requires, + Stop: Stop, + // Action commands + Discard: Discard, + FileInto: FileInto, + Keep: Keep, + Redirect: Redirect +}; + +})(this.Sieve); diff --git a/dev/Sieve/Extensions/Body.js b/dev/Sieve/Extensions/Body.js new file mode 100644 index 000000000..3b33f065f --- /dev/null +++ b/dev/Sieve/Extensions/Body.js @@ -0,0 +1,28 @@ +/** + * https://tools.ietf.org/html/rfc5173 + */ + +(Sieve => { + +Sieve.Extensions.Body = class +{ + constructor() + { + this.comparator = 'i;ascii-casemap', + this.match_type = ':is', + this.body_transform = ''; // :raw, :content , :text + this.key_list = new Sieve.Grammar.StringList; + } + + toString() + { + return 'body' +// + ' ' + this.comparator + + ' ' + this.match_type + + ' ' + this.body_transform + + ' ' + this.key_list; + } +}; + +})(this.Sieve); + diff --git a/dev/Sieve/Extensions/Ereject.js b/dev/Sieve/Extensions/Ereject.js new file mode 100644 index 000000000..74b938889 --- /dev/null +++ b/dev/Sieve/Extensions/Ereject.js @@ -0,0 +1,30 @@ +/** + * https://tools.ietf.org/html/rfc5429#section-2.1 + */ + +(Sieve => { + +Sieve.Extensions.Ereject = class +{ + constructor() + { + this.reason = new Sieve.Grammar.QuotedString; + } + + toString() + { + return 'ereject ' + this.reason; + } + + get reason() + { + return this.reason.value; + } + + set reason(value) + { + this.reason.value = value; + } +}; + +})(this.Sieve); diff --git a/dev/Sieve/Extensions/Imap4flags.js b/dev/Sieve/Extensions/Imap4flags.js new file mode 100644 index 000000000..5d4f96061 --- /dev/null +++ b/dev/Sieve/Extensions/Imap4flags.js @@ -0,0 +1,17 @@ +/** + * https://tools.ietf.org/html/rfc5232 + */ + +(Sieve => { + +Sieve.Extensions.Imap4flags = class +{ + /** + * setflag [] + * addflag [] + * removeflag [] + * hasflag [MATCH-TYPE] [COMPARATOR] [] + */ +}; + +})(this.Sieve); diff --git a/dev/Sieve/Extensions/Reject.js b/dev/Sieve/Extensions/Reject.js new file mode 100644 index 000000000..fe6505068 --- /dev/null +++ b/dev/Sieve/Extensions/Reject.js @@ -0,0 +1,30 @@ +/** + * https://tools.ietf.org/html/rfc5429#section-2.2 + */ + +(Sieve => { + +Sieve.Extensions.Reject = class +{ + constructor() + { + this.reason = new Sieve.Grammar.QuotedString; + } + + toString() + { + return 'reject ' + this.reason; + } + + get reason() + { + return this.reason.value; + } + + set reason(value) + { + this.reason.value = value; + } +}; + +})(this.Sieve); diff --git a/dev/Sieve/Extensions/Vacation.js b/dev/Sieve/Extensions/Vacation.js new file mode 100644 index 000000000..7129bbc00 --- /dev/null +++ b/dev/Sieve/Extensions/Vacation.js @@ -0,0 +1,84 @@ +/** + * https://tools.ietf.org/html/rfc5230 + */ + +(Sieve => { + +Sieve.Extensions.Vacation = class +{ + constructor() + { + this.arguments = { + ':days' : new Sieve.Grammar.Number, + ':subject' : new Sieve.Grammar.QuotedString, + ':from' : new Sieve.Grammar.QuotedString, + ':addresses': new Sieve.Grammar.StringList, + ':mime' : false, + ':handle' : new Sieve.Grammar.QuotedString + }; + this.reason = ''; // QuotedString / MultiLine + } + + toString() + { + let result = 'vacation'; + if (0 < this.arguments[':days'].value) { + result += ' :days ' + this.arguments[':days']; + } + if (this.arguments[':subject'].length()) { + result += ' :subject ' + this.arguments[':subject']; + } + if (this.arguments[':from'].length()) { + result += ' :from ' + this.arguments[':from']; + } + if (this.arguments[':addresses'].length) { + result += ' :addresses ' + this.arguments[':addresses']; + } + if (this.arguments[':mime']) { + result += ' :mime'; + } + if (this.arguments[':handle'].length()) { + result += ' :handle ' + this.arguments[':handle']; + } + return result + ' ' + this.reason; + } +/* + function __get($key) + { + if ('reason' === $key) { + return this.reason; + } + if (isset(this.arguments[":{$key}"])) { + return this.arguments[":{$key}"]; + } + } + + function __set($key, $value) + { + if ('days' === $key) { + this.arguments[":{$key}"] = (int) $value; + } else if ('mime' === $key) { + this.arguments[":{$key}"] = (bool) $value; + } else if ('reason' === $key) { + this.reason = (string) $value; + } else if ('addresses' !== $key && isset(this.arguments[":{$key}"])) { + this.arguments[":{$key}"] = (string) $value; + } + } + + public function pushArguments(array $args): void + { + foreach ($args as $i => $arg) { + if (\in_array($arg, [':days',':subject',':from',':addresses', ':handle'])) { + this.arguments[$arg] = $args[$i+1]; + } else if (':mime' === $arg) { + this.arguments[':mime'] = true; + } else if (!isset($args[$i+1])) { + this.reason = $arg; + } + } + } +*/ +}; + +})(this.Sieve); diff --git a/dev/Sieve/Grammar.js b/dev/Sieve/Grammar.js new file mode 100644 index 000000000..308b4c99d --- /dev/null +++ b/dev/Sieve/Grammar.js @@ -0,0 +1,286 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-8.2 + */ + +(Sieve => { + +/** + * abstract + */ +class StringType /*extends String*/ +{ + constructor(value = '') + { + this._value = value; + } + +// abstract function toString(); + + get value() + { + return this._value; + } + + set value(value) + { + this._value = value; + } + + length() + { + return this._value.length; + } +} + +/** + * abstract + */ +class Comment extends StringType +{ +} + +/** + * https://tools.ietf.org/html/rfc5228#section-2.9 + */ +class Command +{ + constructor(identifier, text = null) + { + this.identifier = identifier; + this.arguments = []; + if (text) { + this.text = text; + } + } + + toString() + { + let result = this.identifier; + if ('anyof' === result || 'allof' === result) { + let tests = []; + this.commands.forEach(cmd => tests.push(cmd.toString().replace(/;$/, ''))); + result += ' (\r\n\t' + tests.join(',\r\n\t') + '\r\n)'; + } else { + this.arguments.forEach(arg => { + if (Array.isArray(arg)) { + result += ' [' + arg.join(',') + ']'; + } else { + result += ' ' + arg; + } + }); + if (this.commands) { + result += ' {\r\n'; + this.commands.forEach(cmd => result += '\t' + cmd + '\r\n'); + result += '}'; + } else { + result += ';'; + } + } + return result; + } + + getComparators() + { + return ['i;ascii-casemap']; + } + + getMatchTypes() + { + return [':is', ':contains', ':matches']; + } + + pushArguments(args) + { + this.arguments = args; + } +} + +class BracketComment extends Comment +{ + toString() + { + return '/* ' + super() + ' */'; + } +} + +class HashComment extends Comment +{ + toString() + { + return '# ' + super(); + } +} + +class NumberType /*extends Number*/ +{ + constructor(value = '0') + { + this._value = value; + } + + toString() + { + return this._value; + } + + get value() + { + return this._value; + } + + set value(value) + { + this._value = value; + } +} + +class StringList extends Array +{ + toString() + { + if (1 < this.length) { + return '[' + this.join(',') + ']'; + } + return this.length ? this[0] : ''; + } + + push(value) + { + if (!(value instanceof StringType)) { + value = new QuotedString(value); + } + super(value); + } +} + +StringList.fromString = list => { + let string, + obj = new StringList, + regex = RegExp('(?:^\\s*|\\s*,\\s*)(' + Sieve.RegEx.STRING + ')', 'g'); + list = list.replace(/^[\r\n\t[]+/, ''); + while ((string = regex.exec(list))) { + if (string[4]) { + obj.push(new MultiLine(string[4], string[3])); + } else { + obj.push(new QuotedString(string[2])); + } + } + return obj; +} + +class QuotedString extends StringType +{ + toString() + { + return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-8.1 + */ +class MultiLine extends StringType +{ + constructor(value, comment = '') + { + super(); + this.value = value; + this.comment = comment; + } + + toString() + { + return 'text:' + + (this.comment ? "#${this.comment}" : '') + "\r\n" +// + \rtrim(this.value, "\r\n") + + "\r\n.\r\n"; + } +} + + +/** + * https://tools.ietf.org/html/rfc5228#section-2.9 + */ +class Test +{ + constructor(identifier) + { + this.identifier = identifier; + this.arguments = []; + } + + toString() + { + return (this.identifier + ' ' + this.arguments.join(' ')).trim(); + } + + pushArguments(args) + { + this.arguments = args; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.2 + * https://tools.ietf.org/html/rfc5228#section-5.3 + */ +class TestList extends Array +{ + constructor(identifier) + { + super(); + this.identifier = identifier; // allof / anyof + } + + toString() + { + if (1 < this.length) { + return '(' + this.join(', ') + ')'; + } + return this.length ? this[0] : ''; + } + + push(value) + { +/* + if (!(value instanceof Command)) { + value = new Command($value); + } +*/ + super(value); + } +} + +TestList.fromString = list => { + let test, + obj = new TestList, + regex = RegExp('(?:^\\s*|\\s*,\\s*)(' + Sieve.RegEx.IDENTIFIER + ')((?:\\s+' + Sieve.RegEx.ARGUMENT + ')*)', 'g'); + list = list.replace(/^[\r\n\t(]+/, ''); + while ((test = regex.exec(list))) { + let command = new Sieve.Commands.Command(test[1]); + obj.push(command); +/* + if (\preg_match_all('@\\s+(' . Sieve.RegEx.ARGUMENT . ')@', $test[2], $args, PREG_SET_ORDER)) { + foreach ($args as $arg) { + $command->arguments[] = $arg; + } + } +*/ + } + return obj; +} + +Sieve.Grammar = { + Command: Command, + BracketComment: BracketComment, + HashComment: HashComment, + MultiLine: MultiLine, + Number: NumberType, + QuotedString: QuotedString, + StringList: StringList, + StringType: StringType, + Test: Test, + TestList: TestList +}; + +})(this.Sieve); diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js new file mode 100644 index 000000000..7b5eea2ff --- /dev/null +++ b/dev/Sieve/Parser.js @@ -0,0 +1,230 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-8 + */ + +(Sieve => { + +const + RegEx = Sieve.RegEx, + + M = { + UNKNOWN : 0, + STRING_LIST : 1, + QUOTED_STRING : 2, + MULTILINE_STRING : 3, + HASH_COMMENT : 4, + BRACKET_COMMENT : 5, + BLOCK_START : 6, + BLOCK_END : 7, + LEFT_PARENTHESIS : 8, + RIGHT_PARENTHESIS: 9, + COMMA : 10, + SEMICOLON : 11, + TAG : 12, + IDENTIFIER : 13, + NUMBER : 14, + WHITESPACE : 15 + }, + + MRegEx = [ + /* M.STRING_LIST */ RegEx.STRING_LIST, + /* M.QUOTED_STRING */ RegEx.QUOTED_STRING, + /* M.MULTILINE_STRING */ RegEx.MULTI_LINE, + /* M.HASH_COMMENT */ RegEx.HASH_COMMENT, + /* M.BRACKET_COMMENT */ RegEx.BRACKET_COMMENT, + /* M.BLOCK_START */ '\\{', + /* M.BLOCK_END */ '\\}', + /* M.LEFT_PARENTHESIS */ '\\(', // anyof / allof + /* M.RIGHT_PARENTHESIS */ '\\)', // anyof / allof + /* M.COMMA */ ',', + /* M.SEMICOLON */ ';', + /* M.TAG */ RegEx.TAG, + /* M.IDENTIFIER */ RegEx.IDENTIFIER, + /* M.NUMBER */ RegEx.NUMBER, + /* M.WHITESPACE */ '(?: |\\r\\n|\\t)+', + /* M.UNKNOWN */ '[^ \\r\\n\\t]+' + ].join(')|('); + +Sieve.parseScript = script => { + let match, + line = 1, + tree = [], + + // create one regex to find the right match + // avoids looping over all possible tokens: increases performance + regex = RegExp(MRegEx, 'g'), + + levels = [], + command = null, + args = []; + + const + error = message => { throw new SyntaxError(message + ' at ' + regex.lastIndex, 'script.sieve', line) }, + pushArgs = () => { + if (command && args.length) { + 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 M.IDENTIFIER: { + pushArgs(); + let new_command; + if ('if' === value) { + new_command = new Sieve.Commands.Conditional(value); + } else if ('elsif' === value || 'else' === value) { + (command instanceof Sieve.Commands.Conditional) + || error('Not after IF condition'); + new_command = new Sieve.Commands.Conditional(value); + } else if ('allof' === value || 'anyof' === value) { + (command instanceof Sieve.Commands.Conditional) + || error('Test-list not in conditional'); + new_command = new Sieve.Tests[value](); + } else if (Sieve.Tests[value]) { + // address / envelope / exists / header / not / size + new_command = new Sieve.Tests[value](); + } else if (Sieve.Commands[value]) { + // discard / fileinto / keep / redirect / require / stop + new_command = new Sieve.Commands[value](); + } else if (Sieve.Extensions[value]) { + // body / ereject / reject / imap4flags / vacation + new_command = new Sieve.Extensions[value](); + } else { + error('Unknown command ' + value); +// new_command = new Sieve.Grammar.Command(value); + } + + if (command instanceof Sieve.Commands.Conditional && !command.test) { + if (new_command instanceof Sieve.Grammar.TestList) { + levels.push(new_command); + } else { + new_command = new Sieve.Grammar.Test(value); + } + command.test = new_command; + } else { + if (levels.length) { + let cmd = levels.last(); + if (cmd instanceof Sieve.Grammar.TestList) { + cmd.push(new_command); + } else { + cmd.commands.push(new_command); + } + } else { + tree.push(new_command); + } + if (new_command instanceof Sieve.Commands.Conditional || new_command instanceof Sieve.Commands.TestList) { + levels.push(new_command); + } + } + command = new_command; + break; } + + case M.TAG: + command + ? args.push(value) + : error('Tag must be command argument'); + break; + case M.STRING_LIST: + command + ? args.push(Sieve.Grammar.StringList.fromString(value)) + : error('String list must be command argument'); + break; + case M.MULTILINE_STRING: + command + ? args.push(new Sieve.Grammar.MultiLine(value)) + : error('Multi-line string must be command argument'); + break; + case M.QUOTED_STRING: + command + ? args.push(new Sieve.Grammar.QuotedString(value.substr(1,value.length-2))) + : error('Quoted string must be command argument'); + break; + case M.NUMBER: + command + ? args.push(new Sieve.Grammar.Number(value)) + : error('Number must be command argument'); + break; + + case M.BRACKET_COMMENT: + (command ? command.commands : tree).push( + new Sieve.Grammar.BracketComment(value.substr(1,value.length-4)) + ); + break; + + case M.HASH_COMMENT: + (command ? command.commands : tree).push( + new Sieve.Grammar.HashComment(value.substr(1).trim()) + ); + break; + + case M.WHITESPACE: +// (command ? command.commands : tree).push(value.trim()); + command || tree.push(value.trim()); + break; + + case M.SEMICOLON: + command || error('Semicolon not at end of command'); + pushArgs(); +// levels.pop(); + command = levels.last(); + break; + + case M.BLOCK_START: + // https://tools.ietf.org/html/rfc5228#section-2.9 + // Action commands do not take tests or blocks + levels.length || error('Block start not part of control command'); + command || error('Block start not at end of command arguments'); + pushArgs(); + command = levels.last(); + break; + case M.BLOCK_END: + levels.length || error('Block end has no matching block start'); + levels.pop(); + command = levels.last(); + break; + + // anyof / allof + case M.LEFT_PARENTHESIS: + (levels.last() instanceof Sieve.Grammar.TestList) + || error('Test start not part of test-list'); + command || error('Not inside command'); + pushArgs(); + command = levels.last(); + break; + case M.RIGHT_PARENTHESIS: + (levels.last() instanceof Sieve.Grammar.TestList) + || error('Test end not part of test-list'); + pushArgs(); + levels.pop(); + command = levels.last(); + break; + case M.COMMA: + // Must be inside PARENTHESIS aka test-list + (levels.last() instanceof Sieve.Grammar.TestList) + || error('Comma not part of test-list'); + pushArgs(); + command = levels.last(); + break; + + case M.UNKNOWN: + error('Invalid token ' + value); + } + + // Set current script position + line += (value.split('\n').length - 1); // (value.match(/\n/g) || []).length; + } + + return tree; +}; + +})(this.Sieve); diff --git a/dev/Sieve/RegEx.js b/dev/Sieve/RegEx.js new file mode 100644 index 000000000..61ee72972 --- /dev/null +++ b/dev/Sieve/RegEx.js @@ -0,0 +1,219 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-8 + */ + +(Sieve => { + +const + /************************************************** + * https://tools.ietf.org/html/rfc5228#section-8.1 + **************************************************/ + + /** + * octet-not-crlf = %x01-09 / %x0B-0C / %x0E-FF + * a single octet other than NUL, CR, or LF + */ + OCTET_NOT_CRLF = '[^\\x00\\r\\n]', + + /** + * octet-not-period = %x01-09 / %x0B-0C / %x0E-2D / %x2F-FF + * a single octet other than NUL, CR, LF, or period + */ + OCTET_NOT_PERIOD = '[^\\x00\\r\\n\\.]', + + /** + * octet-not-qspecial = %x01-09 / %x0B-0C / %x0E-21 / %x23-5B / %x5D-FF + * a single octet other than NUL, CR, LF, double-quote, or backslash + */ + OCTET_NOT_QSPECIAL = '[^\\x00\\r\\n"\\\\]', + + /** + * QUANTIFIER = "K" / "M" / "G" + */ + QUANTIFIER = '[KMGkmg]', + + /** + * quoted-special = "\" (DQUOTE / "\") + * represents just a double-quote or backslash + */ + QUOTED_SPECIAL = '\\\\\\\\|\\\\"', + + /** + * quoted-text = *(quoted-safe / quoted-special / quoted-other) + */ + QUOTED_TEXT = '(?:' + QUOTED_SAFE + '|' + QUOTED_SPECIAL + ')*', + + /** + * quoted-safe = CRLF / octet-not-qspecial + * either a CRLF pair, OR a single octet other than NUL, CR, LF, double-quote, or backslash + */ + QUOTED_SAFE = '\\r\\n|' + OCTET_NOT_QSPECIAL, + + /** + * multiline-literal = [ octet-not-period *octet-not-crlf ] CRLF + */ + MULTILINE_LITERAL = OCTET_NOT_PERIOD + OCTET_NOT_CRLF + '*\\r\\n', + + /** + * multiline-dotstart = "." 1*octet-not-crlf CRLF + ; A line containing only "." ends the multi-line. + ; Remove a leading '.' if followed by another '.'. + */ + MULTILINE_DOTSTART = '\\.' + OCTET_NOT_CRLF + '+\\r\\n', + + /** + * not-star = CRLF / %x01-09 / %x0B-0C / %x0E-29 / %x2B-FF + * either a CRLF pair, OR a single octet other than NUL, CR, LF, or star + */ +// NOT_STAR: '\\r\\n|[^\\x00\\r\\n*]', + + /** + * not-star-slash = CRLF / %x01-09 / %x0B-0C / %x0E-29 / %x2B-2E / %x30-FF + * either a CRLF pair, OR a single octet other than NUL, CR, LF, star, or slash + */ +// NOT_STAR_SLASH: '\\r\\n|[^\\x00\\r\\n*\\\\]', + + /** + * STAR = "*" + */ +// STAR = '\\*', + + RegEx = { + + /** + * bracket-comment = "/*" *not-star 1*STAR *(not-star-slash *not-star 1*STAR) "/" + */ + BRACKET_COMMENT: '/\\*.*?\\*/', // '/\\*[\\s\\S]*?\\*/' + + /** + * hash-comment = "#" *octet-not-crlf CRLF + */ + HASH_COMMENT: '#' + OCTET_NOT_CRLF + '*\\r\\n', + + /** + * identifier = (ALPHA / "_") *(ALPHA / DIGIT / "_") + */ + IDENTIFIER: '[a-zA-Z_][a-zA-Z0-9_]*', + + /** + * number = 1*DIGIT [ QUANTIFIER ] + */ + NUMBER: '[0-9]+' + QUANTIFIER + '?', + + /** + * quoted-string = DQUOTE quoted-text DQUOTE + */ + QUOTED_STRING: '"' + QUOTED_TEXT + '"', + + /** + * tag = ":" identifier + */ + TAG: ':[a-zA-Z_][a-zA-Z0-9_]*', + + /************************************************** + * https://tools.ietf.org/html/rfc5228#section-8.3 + **************************************************/ + + /** + * ADDRESS-PART = ":localpart" / ":domain" / ":all" + */ +// ADDRESS_PART: ':localpart|:domain|:all', + + /** + * MATCH-TYPE = ":is" / ":contains" / ":matches" + */ +// MATCH_TYPE: ':is|:contains|:matches' + + }; + +/** + * comment = bracket-comment / hash-comment + */ +//RegEx.COMMENT = RegEx.BRACKET_COMMENT + '|' + RegEx.HASH_COMMENT; + +/** + * multi-line = "text:" *(SP / HTAB) (hash-comment / CRLF) + *(multiline-literal / multiline-dotstart) + "." CRLF + */ +RegEx.MULTI_LINE = 'text:[ \\t]*(?:' + RegEx.HASH_COMMENT + ')?\\r\\n' + + '(?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*' + + '\\.\\r\\n'; + +/************************************************** + * https://tools.ietf.org/html/rfc5228#section-8.2 + **************************************************/ + + /** + * string = quoted-string / multi-line + */ + RegEx.STRING = RegEx.QUOTED_STRING + '|' + RegEx.MULTI_LINE; + + /** + * string-list = "[" string *("," string) "]" / string + * if there is only a single string, the brackets are optional + */ + RegEx.STRING_LIST = '\\[\\s*(?:' + RegEx.STRING + ')(?:\\s*,\\s*(?:' + RegEx.STRING + '))*\\s*\\]' + + '|(?:' + RegEx.STRING + ')'; + + /** + * argument = string-list / number / tag + */ + RegEx.ARGUMENT = RegEx.STRING_LIST + '|' + RegEx.NUMBER + '|' + RegEx.TAG; + + /** + * arguments = *argument [ test / test-list ] + * This is not possible with regular expressions + */ +// ARGUMENTS = '(?:\\s+' . self::ARGUMENT . ')*(\\s+?:' . self::TEST . '|' . self::TEST_LIST . ')?'; + + /** + * block = "{" commands "}" + * This is not possible with regular expressions + */ +// BLOCK = '{' . self::COMMANDS . '}'; + + /** + * command = identifier arguments (";" / block) + * This is not possible with regular expressions + */ +// COMMAND = self::IDENTIFIER . self::ARGUMENTS . '\\s+(?:;|' . self::BLOCK . ')'; + + /** + * commands = *command + * This is not possible with regular expressions + */ +// COMMANDS = '(?:' . self::COMMAND . ')*'; + + /** + * start = commands + * This is not possible with regular expressions + */ +// START = self::COMMANDS; + + /** + * test = identifier arguments + * This is not possible with regular expressions + */ +// TEST = self::IDENTIFIER . self::ARGUMENTS; + + /** + * test-list = "(" test *("," test) ")" + * This is not possible with regular expressions + */ +// TEST_LIST = '\\(\\s*' . self::TEST . '(?:\\s*,\\s*' . self::TEST . ')*\\s*\\)'; + + +/************************************************** + * https://tools.ietf.org/html/rfc5228#section-8.3 + **************************************************/ + + /** + * COMPARATOR = ":comparator" string + */ +// RegEx.COMPARATOR = ':comparator\\s+(?:' + RegEx.STRING + ')'; + + +Sieve.RegEx = RegEx; + +})(this.Sieve); diff --git a/dev/Sieve/Tests.js b/dev/Sieve/Tests.js new file mode 100644 index 000000000..38ea0591a --- /dev/null +++ b/dev/Sieve/Tests.js @@ -0,0 +1,218 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-5 + */ + +(Sieve => { + +/** + * https://tools.ietf.org/html/rfc5228#section-5.1 + */ +class Address +{ + constructor() + { + this.comparator = 'i;ascii-casemap'; + this.address_part = ':all'; // :localpart | :domain | :all + this.match_type = ':is'; + this.header_list = new Sieve.Grammar.StringList; + this.key_list = new Sieve.Grammar.StringList; + } + + toString() + { + return 'address' +// + ' ' + this.comparator + + ' ' + this.address_part + + ' ' + this.match_type + + ' ' + this.header_list + + ' ' + this.key_list; + } + + pushArguments(/*args*/) + { + throw 'TODO'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.2 + */ +class AllOf extends Sieve.Grammar.TestList +{ + constructor() + { + super('allof'); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.3 + */ +class AnyOf extends Sieve.Grammar.TestList +{ + constructor() + { + super('anyof'); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.4 + */ +class Envelope +{ + constructor() + { + this.comparator = 'i;ascii-casemap'; + this.address_part = ':all'; // :localpart | :domain | :all + this.match_type = ':is'; + this.envelope_part = new Sieve.Grammar.StringList; + this.key_list = new Sieve.Grammar.StringList; + } + + toString() + { + return 'envelope' +// + ' ' + this.comparator + + ' ' + this.address_part + + ' ' + this.match_type + + ' ' + this.envelope_part + + ' ' + this.key_list; + } + + pushArguments(/*args*/) + { + throw 'TODO'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.5 + */ +class Exists +{ + constructor() + { + this.header_names = new Sieve.Grammar.StringList; + } + + toString() + { + return "exists {this.header_names}"; + } + + pushArguments(/*args*/) + { + throw 'TODO'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.6 + */ +class False +{ + toString() + { + return "false"; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.7 + */ +class Header +{ + constructor() + { + this.comparator = 'i;ascii-casemap'; + this.address_part = ':all'; // :localpart | :domain | :all + this.match_type = ':is'; + this.header_names = new Sieve.Grammar.StringList; + this.key_list = new Sieve.Grammar.StringList; + } + + toString() + { + return 'header' +// + ' ' + this.comparator + + ' ' + this.match_type + + ' ' + this.header_names + + ' ' + this.key_list; + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':is' === arg || ':contains' === arg || ':matches' === arg) { + this.match_type = arg; + } else if (arg instanceof Sieve.Grammar.StringList || arg instanceof Sieve.Grammar.StringType) { + this[args[i+1] ? 'header_names' : 'key_list'] = arg; +// (args[i+1] ? this.header_names : this.key_list) = arg; + } + }); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.8 + */ +class Not +{ + constructor() + { + this.test = new Sieve.Grammar.Test; + } + + toString() + { + return 'not ' + this.test; + } + + pushArguments(/*args*/) + { + throw 'TODO'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.9 + */ +class Size +{ + constructor() + { + this.mode = ':over'; // :under + this.limit = 0; + } + + toString() + { + return 'size ' + this.mode + ' ' + this.limit; + } + + pushArguments(args) + { + args.forEach(arg => { + if (':over' === arg || ':under' === arg) { + this.mode = arg; + } else if (arg instanceof Sieve.Grammar.Number) { + this.limit = arg; + } + }); + } +} + +Sieve.Tests = { + Address: Address, + AllOf: AllOf, + AnyOf: AnyOf, + Envelope: Envelope, + Exists: Exists, + False: False, + Header: Header, + Not: Not, + Size: Size +}; + +})(this.Sieve); diff --git a/dev/sieve.js b/dev/sieve.js new file mode 100644 index 000000000..f36f6425d --- /dev/null +++ b/dev/sieve.js @@ -0,0 +1,15 @@ + +(win => { + + win.Sieve = { +/* + RegEx: {}, + Grammar: {}, + Commands: {}, + Tests: {}, + parseScript: ()=>{}, +*/ + Extensions: {} + }; + +})(this); diff --git a/tasks/config.js b/tasks/config.js index e5a3e9980..8fa928640 100644 --- a/tasks/config.js +++ b/tasks/config.js @@ -83,6 +83,18 @@ config.paths.js = { 'dev/External/SquireUI.js' ] }, + sieve: { + name: 'sieve.js', + src: [ + 'dev/sieve.js', + 'dev/Sieve/RegEx.js', + 'dev/Sieve/Grammar.js', + 'dev/Sieve/Commands.js', + 'dev/Sieve/Tests.js', + 'dev/Sieve/Extensions/*.js', + 'dev/Sieve/Parser.js' + ] + }, app: { name: 'app.js' }, diff --git a/tasks/js.js b/tasks/js.js index 218b20d57..16b53c642 100644 --- a/tasks/js.js +++ b/tasks/js.js @@ -49,6 +49,18 @@ const jsLibs = () => { .pipe(gulp.dest(config.paths.staticJS)); }; +// sieve +const jsSieve = () => { + const src = config.paths.js.sieve.src; + return gulp + .src(src) + .pipe(expect.real({ errorOnFailure: true }, src)) + .pipe(concat(config.paths.js.sieve.name, { separator: '\n\n' })) + .pipe(eol('\n', true)) + .pipe(replace(/sourceMappingURL=[a-z0-9.\-_]{1,20}\.map/gi, '')) + .pipe(gulp.dest(config.paths.staticJS)); +}; + // app const jsApp = () => gulp @@ -120,7 +132,7 @@ const jsLint = () => .pipe(eslint.failAfterError()); const jsState1 = gulp.series(jsLint); -const jsState3 = gulp.parallel(jsBoot, jsServiceWorker, jsLibs, jsApp, jsAdmin); +const jsState3 = gulp.parallel(jsBoot, jsServiceWorker, jsLibs, jsSieve, jsApp, jsAdmin); const jsState2 = gulp.series(jsClean, webpack, jsState3, jsMin); exports.jsLint = jsLint;