From d9865e3a469981fd71ff7a68613070b5e560b9ac Mon Sep 17 00:00:00 2001 From: djmaze Date: Thu, 14 Jan 2021 23:42:46 +0100 Subject: [PATCH] Simplify Sieve Parser and added RFC5235 --- dev/Sieve/Commands.js | 53 ++++++-- dev/Sieve/Extensions/Imap4flags.js | 17 --- dev/Sieve/Extensions/{Body.js => rfc5173.js} | 4 +- .../Extensions/{Vacation.js => rfc5230.js} | 17 +-- dev/Sieve/Extensions/rfc5232.js | 114 ++++++++++++++++++ dev/Sieve/Extensions/rfc5235.js | 95 +++++++++++++++ .../Extensions/{Reject.js => rfc5429.js} | 6 +- dev/Sieve/Parser.js | 66 +++++----- dev/Sieve/Tests.js | 24 ++-- dev/sieve.js | 3 - tasks/js.js | 1 - 11 files changed, 308 insertions(+), 92 deletions(-) delete mode 100644 dev/Sieve/Extensions/Imap4flags.js rename dev/Sieve/Extensions/{Body.js => rfc5173.js} (94%) rename dev/Sieve/Extensions/{Vacation.js => rfc5230.js} (83%) create mode 100644 dev/Sieve/Extensions/rfc5232.js create mode 100644 dev/Sieve/Extensions/rfc5235.js rename dev/Sieve/Extensions/{Reject.js => rfc5429.js} (89%) diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js index 76f30ebbe..29d030e97 100644 --- a/dev/Sieve/Commands.js +++ b/dev/Sieve/Commands.js @@ -23,9 +23,7 @@ class Conditional extends Command toString() { - return this.identifier - + ('else' !== this.identifier ? ' ' + this.test : '') - + ' ' + this.commands; + return this.identifier + ' ' + this.test + ' ' + this.commands; } /* public function pushArguments(array $args): void @@ -36,6 +34,35 @@ class Conditional extends Command */ } +class If extends Conditional +{ + constructor() + { + super('if'); + } +} + +class ElsIf extends Conditional +{ + constructor() + { + super('elsif'); + } +} + +class Else extends Conditional +{ + constructor() + { + super('else'); + } + + toString() + { + return this.identifier + ' ' + this.commands; + } +} + /** * https://tools.ietf.org/html/rfc5228#section-3.2 */ @@ -83,13 +110,12 @@ class Stop extends Command */ class FileInto extends Command { -// const REQUIRE = 'fileinto'; - constructor() { super('fileinto'); // QuotedString / MultiLine this._mailbox = new Grammar.QuotedString(); +// this.require = 'fileinto'; } toString() @@ -177,14 +203,17 @@ class Discard extends Command Sieve.Commands = { // Control commands - Conditional: Conditional, - Require: Require, - Stop: Stop, + if: If, + elsif: ElsIf, + else: Else, + conditional: Conditional, + require: Require, + stop: Stop, // Action commands - Discard: Discard, - Fileinto: FileInto, - Keep: Keep, - Redirect: Redirect + discard: Discard, + fileinto: FileInto, + keep: Keep, + redirect: Redirect }; })(this.Sieve); diff --git a/dev/Sieve/Extensions/Imap4flags.js b/dev/Sieve/Extensions/Imap4flags.js deleted file mode 100644 index 5d4f96061..000000000 --- a/dev/Sieve/Extensions/Imap4flags.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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/Body.js b/dev/Sieve/Extensions/rfc5173.js similarity index 94% rename from dev/Sieve/Extensions/Body.js rename to dev/Sieve/Extensions/rfc5173.js index b8ae60123..24a0c104a 100644 --- a/dev/Sieve/Extensions/Body.js +++ b/dev/Sieve/Extensions/rfc5173.js @@ -15,6 +15,7 @@ class Body extends Grammar.Test this.match_type = ':is', this.body_transform = ''; // :raw, :content , :text this.key_list = new Grammar.StringList; +// this.require = 'body'; } toString() @@ -26,7 +27,6 @@ class Body extends Grammar.Test + ' ' + this.key_list; } - pushArguments(args) { args.forEach((arg, i) => { @@ -43,7 +43,7 @@ class Body extends Grammar.Test } } -Sieve.Extensions.Body = Body; +Sieve.Commands.body = Body; })(this.Sieve); diff --git a/dev/Sieve/Extensions/Vacation.js b/dev/Sieve/Extensions/rfc5230.js similarity index 83% rename from dev/Sieve/Extensions/Vacation.js rename to dev/Sieve/Extensions/rfc5230.js index b72c2969d..39a02a22b 100644 --- a/dev/Sieve/Extensions/Vacation.js +++ b/dev/Sieve/Extensions/rfc5230.js @@ -4,20 +4,23 @@ (Sieve => { -class Vacation extends Sieve.Grammar.Command +const Grammar = Sieve.Grammar; + +class Vacation extends Grammar.Command { constructor() { super('vacation'); this.arguments = { - ':days' : new Sieve.Grammar.Number, - ':subject' : new Sieve.Grammar.QuotedString, - ':from' : new Sieve.Grammar.QuotedString, - ':addresses': new Sieve.Grammar.StringList, + ':days' : new Grammar.Number, + ':subject' : new Grammar.QuotedString, + ':from' : new Grammar.QuotedString, + ':addresses': new Grammar.StringList, ':mime' : false, - ':handle' : new Sieve.Grammar.QuotedString + ':handle' : new Grammar.QuotedString }; this.reason = ''; // QuotedString / MultiLine +// this.require = 'vacation'; } toString() @@ -82,6 +85,6 @@ class Vacation extends Sieve.Grammar.Command */ } -Sieve.Extensions.Vacation = Vacation; +Sieve.Commands.vacation = Vacation; })(this.Sieve); diff --git a/dev/Sieve/Extensions/rfc5232.js b/dev/Sieve/Extensions/rfc5232.js new file mode 100644 index 000000000..186d4ae30 --- /dev/null +++ b/dev/Sieve/Extensions/rfc5232.js @@ -0,0 +1,114 @@ +/** + * https://tools.ietf.org/html/rfc5232 + */ + +(Sieve => { + +const Grammar = Sieve.Grammar; + +class Flag extends Grammar.Command +{ + constructor(identifier) + { + super(identifier); + this._variablename = new Grammar.QuotedString; + this.list_of_flags = new Grammar.StringList; +// this.require = 'imap4flags'; + } + + toString() + { + return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags + ';'; + } + + get variablename() + { + return this._variablename.value; + } + + set variablename(value) + { + this._variablename.value = value; + } + + pushArguments(args) + { + if (args[1]) { + if (args[0] instanceof Grammar.QuotedString) { + this._variablename = args[0]; + } + if (args[1] instanceof Grammar.StringType) { + this.list_of_flags = args[1]; + } + } else if (args[0] instanceof Grammar.StringType) { + this.list_of_flags = args[0]; + } + } +} + +class SetFlag extends Flag +{ + constructor() + { + super('setflag'); + } +} + +class AddFlag extends Flag +{ + constructor() + { + super('addflag'); + } +} + +class RemoveFlag extends Flag +{ + constructor() + { + super('removeflag'); + } +} + +class HasFlag extends Grammar.Test +{ + constructor() + { + super('hasflag'); + this.comparator = 'i;ascii-casemap', + this.match_type = ':is', + this.variable_list = new Grammar.StringList; + this.list_of_flags = new Grammar.StringList; +// this.require = 'imap4flags'; + } + + toString() + { + return 'hasflag' + + ' ' + this.match_type +// + ' ' + this.comparator + + ' ' + this.variable_list + + ' ' + this.list_of_flags; + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':is' === arg || ':contains' === arg || ':matches' === arg) { + this.match_type = arg; + } else if (arg instanceof Grammar.StringList || arg instanceof Grammar.StringType) { + this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg; + } + }); + } +} + +Object.assign(Sieve.Commands, { + setflag: SetFlag, + addflag: AddFlag, + removeflag: RemoveFlag, + hasflag: HasFlag +// mark and unmark never made it into the RFC +}); + +})(this.Sieve); diff --git a/dev/Sieve/Extensions/rfc5235.js b/dev/Sieve/Extensions/rfc5235.js new file mode 100644 index 000000000..08a610efe --- /dev/null +++ b/dev/Sieve/Extensions/rfc5235.js @@ -0,0 +1,95 @@ +/** + * https://tools.ietf.org/html/rfc5235 + */ + +(Sieve => { + +const Grammar = Sieve.Grammar; + +class SpamTest extends Grammar.Test +{ + constructor() + { + super('spamtest'); + this.percent = false, // 0 - 100 else 0 - 10 + this.comparator = 'i;ascii-casemap', + this.match_type = ':is', + this.value = new Grammar.QuotedString; +// this.require = this.percent ? 'spamtestplus' : 'spamtest'; + } + + toString() + { + return 'spamtest' + + (this.percent ? ' :percent' : '') +// + ' ' + this.comparator + + ' ' + this.match_type + + ' ' + this.value; + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':is' === arg || ':contains' === arg || ':matches' === arg) { + this.match_type = arg; + } else if (':percent' === arg) { + this.percent = true; + } else if (arg instanceof Grammar.StringType) { + if (':comparator' === args[i-1]) { + this.comparator = arg; + } else if (':value' === args[i-1] || ':count' === args[i-1]) { + // Sieve relational [RFC5231] match types + this.match_type = args[i-1] + ' ' + arg; + } else { + this.value = arg; + } + } + }); + } +} + +class VirusTest extends Grammar.Test +{ + constructor() + { + super('virustest'); + this.comparator = 'i;ascii-casemap', + this.match_type = ':is', + this.value = new Grammar.QuotedString; // 1 - 5 +// this.require = 'virustest'; + } + + toString() + { + return 'virustest' +// + ' ' + this.comparator + + ' ' + this.match_type + + ' ' + this.value; + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':is' === arg || ':contains' === arg || ':matches' === arg) { + this.match_type = arg; + } else if (arg instanceof Grammar.StringType) { + if (':comparator' === args[i-1]) { + this.comparator = arg; + } else if (':value' === args[i-1] || ':count' === args[i-1]) { + // Sieve relational [RFC5231] match types + this.match_type = args[i-1] + ' ' + arg; + } else { + this.value = arg; + } + } + }); + } +} + +Object.assign(Sieve.Commands, { + spamtest: SpamTest, + virustest: VirusTest +}); + + +})(this.Sieve); diff --git a/dev/Sieve/Extensions/Reject.js b/dev/Sieve/Extensions/rfc5429.js similarity index 89% rename from dev/Sieve/Extensions/Reject.js rename to dev/Sieve/Extensions/rfc5429.js index 061564161..f956a06dc 100644 --- a/dev/Sieve/Extensions/Reject.js +++ b/dev/Sieve/Extensions/rfc5429.js @@ -15,6 +15,7 @@ class Ereject extends Grammar.Command { super('ereject'); this._reason = new Grammar.QuotedString; +// this.require = 'ereject'; } toString() @@ -49,6 +50,7 @@ class Reject extends Grammar.Command { super('reject'); this._reason = new Grammar.QuotedString; +// this.require = 'reject'; } toString() @@ -74,7 +76,7 @@ class Reject extends Grammar.Command } } -Sieve.Extensions.Ereject = Ereject; -Sieve.Extensions.Reject = Reject; +Sieve.Commands.ereject = Ereject; +Sieve.Commands.reject = Reject; })(this.Sieve); diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js index 6cb7a2f82..15a4f3780 100644 --- a/dev/Sieve/Parser.js +++ b/dev/Sieve/Parser.js @@ -6,6 +6,8 @@ const RegEx = Sieve.RegEx, + Grammar = Sieve.Grammar, + Commands = Sieve.Commands, T_UNKNOWN = 0, T_STRING_LIST = 1, @@ -80,43 +82,35 @@ Sieve.parseScript = script => { case T_IDENTIFIER: { pushArgs(); value = value.toLowerCase(); - let new_command, - className = value[0].toUpperCase() + value.substring(1); + let new_command; if ('if' === value) { - new_command = new Sieve.Commands.Conditional(value); + new_command = new Commands.conditional(value); } else if ('elsif' === value || 'else' === value) { -// (prev_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[className](); - } else if (Sieve.Tests[className]) { - // address / envelope / exists / header / not / size - new_command = new Sieve.Tests[className](); - } else if (Sieve.Commands[className]) { - // discard / fileinto / keep / redirect / require / stop - new_command = new Sieve.Commands[className](); - } else if (Sieve.Extensions[className]) { - // body / ereject / reject / imap4flags / vacation - new_command = new Sieve.Extensions[className](); +// (prev_command instanceof Commands.conditional) || error('Not after IF condition'); + new_command = new Commands.conditional(value); + } else if (Commands[value]) { + if ('allof' === value || 'anyof' === value) { + (command instanceof Commands.conditional) || error('Test-list not in conditional'); + } + new_command = new Commands[value](); } else { console.error('Unknown command: ' + value); if (command && ( - command instanceof Sieve.Commands.Conditional - || command instanceof Sieve.Tests.Not - || command.tests instanceof Sieve.Grammar.TestList)) { - new_command = new Sieve.Grammar.Test(value); + command instanceof Commands.conditional + || command instanceof Commands.not + || command.tests instanceof Grammar.TestList)) { + new_command = new Grammar.Test(value); } else { - new_command = new Sieve.Grammar.Command(value); + new_command = new Grammar.Command(value); } } - if (new_command instanceof Sieve.Grammar.Test) { - if (command instanceof Sieve.Commands.Conditional || command instanceof Sieve.Tests.Not) { + if (new_command instanceof Grammar.Test) { + if (command instanceof Commands.conditional || command instanceof Commands.not) { // if/elsif/else new_command // not new_command command.test = new_command; - } else if (command.tests instanceof Sieve.Grammar.TestList) { + } else if (command.tests instanceof Grammar.TestList) { // allof/anyof .tests[] new_command command.tests.push(new_command); } else { @@ -144,35 +138,35 @@ Sieve.parseScript = script => { break; case T_STRING_LIST: command - ? args.push(Sieve.Grammar.StringList.fromString(value)) + ? args.push(Grammar.StringList.fromString(value)) : error('String list must be command argument'); break; case T_MULTILINE_STRING: command - ? args.push(new Sieve.Grammar.MultiLine(value)) + ? args.push(new Grammar.MultiLine(value)) : error('Multi-line string must be command argument'); break; case T_QUOTED_STRING: command - ? args.push(new Sieve.Grammar.QuotedString(value.substr(1,value.length-2))) + ? args.push(new Grammar.QuotedString(value.substr(1,value.length-2))) : error('Quoted string must be command argument'); break; case T_NUMBER: command - ? args.push(new Sieve.Grammar.Number(value)) + ? args.push(new Grammar.Number(value)) : error('Number must be command argument'); break; // Comments case T_BRACKET_COMMENT: (command ? command.commands : tree).push( - new Sieve.Grammar.BracketComment(value.substr(2, value.length-4)) + new Grammar.BracketComment(value.substr(2, value.length-4)) ); break; case T_HASH_COMMENT: (command ? command.commands : tree).push( - new Sieve.Grammar.HashComment(value.substr(1).trim()) + new Grammar.HashComment(value.substr(1).trim()) ); break; @@ -194,14 +188,14 @@ Sieve.parseScript = script => { pushArgs(); // https://tools.ietf.org/html/rfc5228#section-2.9 // Action commands do not take tests or blocks - while (command && !(command instanceof Sieve.Commands.Conditional)) { + while (command && !(command instanceof Commands.conditional)) { levels.pop(); command = levels.last(); } command || error('Block start not part of control command'); break; case T_BLOCK_END: - (command instanceof Sieve.Commands.Conditional) || error('Block end has no matching block start'); + (command instanceof Commands.conditional) || error('Block end has no matching block start'); levels.pop(); // prev_command = command; command = levels.last(); @@ -210,7 +204,7 @@ Sieve.parseScript = script => { // anyof / allof ( ... , ... ) case T_LEFT_PARENTHESIS: pushArgs(); - while (command && !(command.tests instanceof Sieve.Grammar.TestList)) { + while (command && !(command.tests instanceof Grammar.TestList)) { levels.pop(); command = levels.last(); } @@ -220,14 +214,14 @@ Sieve.parseScript = script => { pushArgs(); levels.pop(); command = levels.last(); - (command.tests instanceof Sieve.Grammar.TestList) || error('Test end not part of test-list'); + (command.tests instanceof Grammar.TestList) || error('Test end not part of test-list'); break; case T_COMMA: pushArgs(); levels.pop(); command = levels.last(); // Must be inside PARENTHESIS aka test-list - (command.tests instanceof Sieve.Grammar.TestList) || error('Comma not part of test-list'); + (command.tests instanceof Grammar.TestList) || error('Comma not part of test-list'); break; case T_UNKNOWN: diff --git a/dev/Sieve/Tests.js b/dev/Sieve/Tests.js index 16ace5089..028038784 100644 --- a/dev/Sieve/Tests.js +++ b/dev/Sieve/Tests.js @@ -236,17 +236,17 @@ class True extends Test } } -Sieve.Tests = { - Address: Address, - Allof: AllOf, - Anyof: AnyOf, - Envelope: Envelope, - Exists: Exists, - False: False, - Header: Header, - Not: Not, - Size: Size, - True: True -}; +Object.assign(Sieve.Commands, { + address: Address, + allof: AllOf, + anyof: AnyOf, + envelope: Envelope, + exists: Exists, + false: False, + header: Header, + not: Not, + size: Size, + true: True +}); })(this.Sieve); diff --git a/dev/sieve.js b/dev/sieve.js index 01e6fc163..c038cf4de 100644 --- a/dev/sieve.js +++ b/dev/sieve.js @@ -6,11 +6,8 @@ RegEx: {}, Grammar: {}, Commands: {}, - Tests: {}, parseScript: ()=>{}, */ - Extensions: {}, - arrayToString: (arr, separator) => arr.map(item => item.toString ? item.toString() : item).join(separator) }; diff --git a/tasks/js.js b/tasks/js.js index 16b53c642..f162b2a62 100644 --- a/tasks/js.js +++ b/tasks/js.js @@ -57,7 +57,6 @@ const jsSieve = () => { .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)); };