From f4cd25f8ad48607c891f1e01df3aee23abeec969 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 9 Mar 2022 12:33:31 +0100 Subject: [PATCH] Added Sieve parser code in master --- dev/Model/SieveScript.js | 7 - dev/Sieve/Commands.js | 173 +++++++++++++++ dev/Sieve/Extensions/rfc5173.js | 47 ++++ dev/Sieve/Extensions/rfc5183.js | 38 ++++ dev/Sieve/Extensions/rfc5229.js | 78 +++++++ dev/Sieve/Extensions/rfc5230.js | 100 +++++++++ dev/Sieve/Extensions/rfc5232.js | 94 ++++++++ dev/Sieve/Extensions/rfc5235.js | 77 +++++++ dev/Sieve/Extensions/rfc5260.js | 106 +++++++++ dev/Sieve/Extensions/rfc5293.js | 91 ++++++++ dev/Sieve/Extensions/rfc5429.js | 81 +++++++ dev/Sieve/Extensions/rfc6609.js | 47 ++++ dev/Sieve/Grammar.js | 277 ++++++++++++++++++++++++ dev/Sieve/Parser.js | 367 ++++++++++++++++++++++++++++++++ dev/Sieve/README.md | 33 +++ dev/Sieve/RegEx.js | 203 ++++++++++++++++++ dev/Sieve/Tests.js | 288 +++++++++++++++++++++++++ dev/Sieve/Utils.js | 4 + dev/View/Popup/SieveScript.js | 9 + tasks/config.js | 3 + tasks/js.js | 11 +- 21 files changed, 2126 insertions(+), 8 deletions(-) create mode 100644 dev/Sieve/Commands.js create mode 100644 dev/Sieve/Extensions/rfc5173.js create mode 100644 dev/Sieve/Extensions/rfc5183.js create mode 100644 dev/Sieve/Extensions/rfc5229.js create mode 100644 dev/Sieve/Extensions/rfc5230.js create mode 100644 dev/Sieve/Extensions/rfc5232.js create mode 100644 dev/Sieve/Extensions/rfc5235.js create mode 100644 dev/Sieve/Extensions/rfc5260.js create mode 100644 dev/Sieve/Extensions/rfc5293.js create mode 100644 dev/Sieve/Extensions/rfc5429.js create mode 100644 dev/Sieve/Extensions/rfc6609.js create mode 100644 dev/Sieve/Grammar.js create mode 100644 dev/Sieve/Parser.js create mode 100644 dev/Sieve/README.md create mode 100644 dev/Sieve/RegEx.js create mode 100644 dev/Sieve/Tests.js create mode 100644 dev/Sieve/Utils.js diff --git a/dev/Model/SieveScript.js b/dev/Model/SieveScript.js index b061dbfbe..a30af59a2 100644 --- a/dev/Model/SieveScript.js +++ b/dev/Model/SieveScript.js @@ -246,13 +246,6 @@ function sieveScriptToFilters(script) } } } -/* - // TODO: branch sieveparser - // https://github.com/the-djmaze/snappymail/tree/sieveparser/dev/Sieve - else if (script.length && window.Sieve) { - let tree = window.Sieve.parseScript(script); - } -*/ return filters; } diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js new file mode 100644 index 000000000..234a35a98 --- /dev/null +++ b/dev/Sieve/Commands.js @@ -0,0 +1,173 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-2.9 + */ + +import { + GrammarCommand, + GrammarString, + GrammarStringList, + GrammarQuotedString +} from 'Sieve/Grammar'; + +/** + * https://tools.ietf.org/html/rfc5228#section-3.1 + * Usage: + * if + * elsif + * else + */ +export class ConditionalCommand extends GrammarCommand +{ + constructor() + { + super(); + this.test = null; + } + + toString() + { + return this.identifier + ' ' + this.test + ' ' + this.commands; + } +/* + public function pushArguments(array $args): void + { + print_r($args); + exit; + } +*/ +} + +export class IfCommand extends ConditionalCommand +{ +} + +export class ElsIfCommand extends ConditionalCommand +{ +} + +export class ElseCommand extends ConditionalCommand +{ + toString() + { + return this.identifier + ' ' + this.commands; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-3.2 + */ +export class RequireCommand extends GrammarCommand +{ + constructor() + { + super(); + this.capabilities = new GrammarStringList(); + } + + toString() + { + return 'require ' + this.capabilities.toString() + ';'; + } + + pushArguments(args) + { + if (args[0] instanceof GrammarStringList) { + this.capabilities = args[0]; + } else if (args[0] instanceof GrammarQuotedString) { + this.capabilities.push(args[0]); + } + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-3.3 + */ +export class StopCommand extends GrammarCommand +{ +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.1 + */ +export class FileIntoCommand extends GrammarCommand +{ + constructor() + { + super(); + // QuotedString / MultiLine + this._mailbox = new GrammarQuotedString(); + } + + get require() { return 'fileinto'; } + + toString() + { + return 'fileinto ' + this._mailbox + ';'; + } + + get mailbox() + { + return this._mailbox.value; + } + + set mailbox(value) + { + this._mailbox.value = value; + } + + pushArguments(args) + { + if (args[0] instanceof GrammarString) { + this._mailbox = args[0]; + } + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.2 + */ +export class RedirectCommand extends GrammarCommand +{ + constructor() + { + super(); + // QuotedString / MultiLine + this._address = new GrammarQuotedString(); + } + + toString() + { + return 'redirect ' + this._address + ';'; + } + + get address() + { + return this._address.value; + } + + set address(value) + { + this._address.value = value; + } + + pushArguments(args) + { + if (args[0] instanceof GrammarString) { + this._address = args[0]; + } + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.3 + */ +export class KeepCommand extends GrammarCommand +{ +} + +/** + * https://tools.ietf.org/html/rfc5228#section-4.4 + */ +export class DiscardCommand extends GrammarCommand +{ +} diff --git a/dev/Sieve/Extensions/rfc5173.js b/dev/Sieve/Extensions/rfc5173.js new file mode 100644 index 000000000..cdf098bb6 --- /dev/null +++ b/dev/Sieve/Extensions/rfc5173.js @@ -0,0 +1,47 @@ +/** + * https://tools.ietf.org/html/rfc5173 + */ + +import { + GrammarString, + GrammarStringList, + GrammarTest +} from 'Sieve/Grammar'; + +export class BodyCommand extends GrammarTest +{ + constructor() + { + super(); + this.body_transform = ''; // :raw, :content , :text + this.key_list = new GrammarStringList; + } + + get require() { return 'body'; } + + toString() + { + return 'body' + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.body_transform + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':raw' === arg || ':text' === arg) { + this.body_transform = arg; + } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) { + if (':content' === args[i-1]) { + this.body_transform = ':content ' + arg; + } else { + this[args[i+1] ? 'content_list' : 'key_list'] = arg; + } + } + }); + } +} + +//Sieve.Commands.body = BodyCommand; diff --git a/dev/Sieve/Extensions/rfc5183.js b/dev/Sieve/Extensions/rfc5183.js new file mode 100644 index 000000000..60107385f --- /dev/null +++ b/dev/Sieve/Extensions/rfc5183.js @@ -0,0 +1,38 @@ +/** + * https://tools.ietf.org/html/rfc5183 + */ + +import { + GrammarQuotedString, + GrammarStringList, + GrammarTest +} from 'Sieve/Grammar'; + +export class EnvironmentCommand extends GrammarTest +{ + constructor() + { + super(); + this.name = new GrammarQuotedString; + this.key_list = new GrammarStringList; + } + + get require() { return 'environment'; } + + toString() + { + return 'environment' + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.name + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + this.name = args[args.length-2]; + this.key_list = args[args.length-1]; + } +} + +//Sieve.Commands.environment = EnvironmentCommand; diff --git a/dev/Sieve/Extensions/rfc5229.js b/dev/Sieve/Extensions/rfc5229.js new file mode 100644 index 000000000..3ebd14a6d --- /dev/null +++ b/dev/Sieve/Extensions/rfc5229.js @@ -0,0 +1,78 @@ +/** + * https://tools.ietf.org/html/rfc5229 + */ + +import { + GrammarCommand, + GrammarQuotedString, + GrammarStringList, + GrammarTest +} from 'Sieve/Grammar'; + +export class SetCommand extends GrammarCommand +{ + constructor() + { + super(); + this.modifiers = []; + this._name = new GrammarQuotedString; + this._value = new GrammarQuotedString; + } + + get require() { return 'variables'; } + + toString() + { + return 'set' + + ' ' + this.modifiers.join(' ') + + ' ' + this._name + + ' ' + this._value; + } + + get name() { return this._name.value; } + set name(str) { this._name.value = str; } + + get value() { return this._value.value; } + set value(str) { this._value.value = str; } + + pushArguments(args) + { + [':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => { + args.includes(modifier) && this.modifiers.push(modifier); + }); + this._name = args[args.length-2]; + this._value = args[args.length-1]; + } +} + +export class StringCommand extends GrammarTest +{ + constructor() + { + super(); + this.source = new GrammarStringList; + this.key_list = new GrammarStringList; + } + + toString() + { + return 'string' + + ' ' + this.match_type + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.source.toString() + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + this.source = args[args.length-2]; + this.key_list = args[args.length-1]; + } +} + +/* +Object.assign(Sieve.Commands, { + set: SetCommand, + string: StringCommand +}); +*/ diff --git a/dev/Sieve/Extensions/rfc5230.js b/dev/Sieve/Extensions/rfc5230.js new file mode 100644 index 000000000..c0baed1a8 --- /dev/null +++ b/dev/Sieve/Extensions/rfc5230.js @@ -0,0 +1,100 @@ +/** + * https://tools.ietf.org/html/rfc5230 + * https://tools.ietf.org/html/rfc6131 + */ + +import { + GrammarCommand, + GrammarNumber, + GrammarQuotedString, + GrammarStringList +} from 'Sieve/Grammar'; + +export class VacationCommand extends GrammarCommand +{ + constructor() + { + super(); + this._days = new GrammarNumber; +// this._seconds = new GrammarNumber; + this._subject = new GrammarQuotedString; + this._from = new GrammarQuotedString; + this.addresses = new GrammarStringList; + this.mime = false; + this._handle = new GrammarQuotedString; + this._reason = new GrammarQuotedString; // QuotedString / MultiLine + } + +// get require() { return ['vacation','vacation-seconds']; } + get require() { return 'vacation'; } + + toString() + { + let result = 'vacation'; + if (0 < this._days.value) { + result += ' :days ' + this._days; +// } else if (0 < this._seconds.value) { +// result += ' :seconds ' + this._seconds; + } + if (this._subject.length) { + result += ' :subject ' + this._subject; + } + if (this._from.length) { + result += ' :from ' + this.arguments[':from']; + } + if (this.addresses.length) { + result += ' :addresses ' + this.addresses.toString(); + } + if (this.mime) { + result += ' :mime'; + } + if (this._handle.length) { + result += ' :handle ' + this._handle; + } + return result + ' ' + this._reason; + } + + get days() { return this._days.value; } +// get seconds() { return this._seconds.value; } + get subject() { return this._subject.value; } + get from() { return this._from.value; } + get handle() { return this._handle.value; } + get reason() { return this._reason.value; } + + set days(int) { this._days.value = int; } +// set seconds(int) { this._seconds.value = int; } + set subject(str) { this._subject.value = str; } + set from(str) { this._from.value = str; } + set handle(str) { this._handle.value = str; } + set reason(str) { this._reason.value = str; } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':mime' === arg) { + this.mime = true; + } else if (i === args.length-1) { + this._reason.value = arg.value; // GrammarQuotedString + } else switch (args[i-1]) { + case ':days': + this._days.value = arg.value; // GrammarNumber + break; +// case ':seconds': +// this._seconds.value = arg.value; // GrammarNumber +// break; + case ':subject': + this._subject.value = arg.value; // GrammarQuotedString + break; + case ':from': + this._from.value = arg.value; // GrammarQuotedString + break; + case ':addresses': + this.addresses = arg; // GrammarStringList + break; + case ':handle': + this._from.value = arg.value; // GrammarQuotedString + break; + } + }); + } +} diff --git a/dev/Sieve/Extensions/rfc5232.js b/dev/Sieve/Extensions/rfc5232.js new file mode 100644 index 000000000..2f9c319bf --- /dev/null +++ b/dev/Sieve/Extensions/rfc5232.js @@ -0,0 +1,94 @@ +/** + * https://tools.ietf.org/html/rfc5232 + */ + +import { + GrammarCommand, + GrammarQuotedString, + GrammarString, + GrammarStringList, + GrammarTest +} from 'Sieve/Grammar'; + +class FlagCommand extends GrammarCommand +{ + constructor() + { + super(); + this._variablename = new GrammarQuotedString; + this.list_of_flags = new GrammarStringList; + } + + get require() { return 'imap4flags'; } + + toString() + { + return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags.toString() + ';'; + } + + get variablename() + { + return this._variablename.value; + } + + set variablename(value) + { + this._variablename.value = value; + } + + pushArguments(args) + { + if (args[1]) { + if (args[0] instanceof GrammarQuotedString) { + this._variablename = args[0]; + } + if (args[1] instanceof GrammarString) { + this.list_of_flags = args[1]; + } + } else if (args[0] instanceof GrammarString) { + this.list_of_flags = args[0]; + } + } +} + +export class SetFlagCommand extends FlagCommand +{ +} + +export class AddFlagCommand extends FlagCommand +{ +} + +export class RemoveFlagCommand extends FlagCommand +{ +} + +export class HasFlagCommand extends GrammarTest +{ + constructor() + { + super(); + this.variable_list = new GrammarStringList; + this.list_of_flags = new GrammarStringList; + } + + get require() { return 'imap4flags'; } + + toString() + { + return 'hasflag' + + ' ' + this.match_type + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.variable_list.toString() + + ' ' + this.list_of_flags.toString(); + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (arg instanceof GrammarStringList || arg instanceof GrammarString) { + this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg; + } + }); + } +} diff --git a/dev/Sieve/Extensions/rfc5235.js b/dev/Sieve/Extensions/rfc5235.js new file mode 100644 index 000000000..3b1112eea --- /dev/null +++ b/dev/Sieve/Extensions/rfc5235.js @@ -0,0 +1,77 @@ +/** + * https://tools.ietf.org/html/rfc5235 + */ + +import { + GrammarQuotedString, + GrammarString, + GrammarTest +} from 'Sieve/Grammar'; + +export class SpamTestCommand extends GrammarTest +{ + constructor() + { + super(); + this.percent = false, // 0 - 100 else 0 - 10 + this.value = new GrammarQuotedString; + } + +// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; } + get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; } + + toString() + { + return 'spamtest' + + (this.percent ? ' :percent' : '') + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.value; + } + + pushArguments(args) + { + args.forEach(arg => { + if (':percent' === arg) { + this.percent = true; + } else if (arg instanceof GrammarString) { + this.value = arg; + } + }); + } +} + +export class VirusTestCommand extends GrammarTest +{ + constructor() + { + super(); + this.value = new GrammarQuotedString; // 1 - 5 + } + + get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; } + + toString() + { + return 'virustest' + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.value; + } + + pushArguments(args) + { + args.forEach(arg => { + if (arg instanceof GrammarString) { + this.value = arg; + } + }); + } +} + +/* +Object.assign(Sieve.Commands, { + spamtest: SpamTestCommand, + virustest: VirusTestCommand +}); +*/ diff --git a/dev/Sieve/Extensions/rfc5260.js b/dev/Sieve/Extensions/rfc5260.js new file mode 100644 index 000000000..d6bdcc571 --- /dev/null +++ b/dev/Sieve/Extensions/rfc5260.js @@ -0,0 +1,106 @@ +/** + * https://tools.ietf.org/html/rfc5260 + */ + +import { + GrammarNumber, + GrammarQuotedString, + GrammarStringList, + GrammarTest +} from 'Sieve/Grammar'; + +export class DateCommand extends GrammarTest +{ + constructor() + { + super(); + this.zone = new GrammarQuotedString; + this.originalzone = false; + this.header_name = new GrammarQuotedString; + this.date_part = new GrammarQuotedString; + this.key_list = new GrammarStringList; + // rfc5260#section-6 + this.index = new GrammarNumber; + this.last = false; + } + +// get require() { return ['date','index']; } + get require() { return 'date'; } + + toString() + { + return 'date' + + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + + (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : '')) + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.header_name + + ' ' + this.date_part + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + let l = args.length - 1; + args.forEach((arg, i) => { + if (':originalzone' === arg) { + this.originalzone = true; + } else if (':last' === arg) { + this.last = true; + } else if (':zone' === args[i-1]) { + this.zone.value = arg.value; + } else if (':index' === args[i-1]) { + this.index.value = arg.value; + } else if (l-2 === i) { + this.header_name = arg; + } else if (l-1 === i) { + this.date_part = arg; + } else if (l === i) { + this.key_list = arg; + } + }); + } +} + +export class CurrentDateCommand extends GrammarTest +{ + constructor() + { + super('date'); + this.zone = new GrammarQuotedString; + this.date_part = new GrammarQuotedString; + this.key_list = new GrammarStringList; + } + + get require() { return 'date'; } + + toString() + { + return 'date' + + (this.zone.length ? ' :zone ' + this.zone : '') + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.date_part + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + let l = args.length - 1; + args.forEach((arg, i) => { + if (':zone' === args[i-1]) { + this.zone.value = arg.value; + } else if (l-1 === i) { + this.date_part = arg; + } else if (l === i) { + this.key_list = arg; + } + }); + } +} + +/* +Object.assign(Sieve.Commands, { + date: DateTestCommand, + currentdate: CurrentDateCommand +*/ diff --git a/dev/Sieve/Extensions/rfc5293.js b/dev/Sieve/Extensions/rfc5293.js new file mode 100644 index 000000000..34ea6989f --- /dev/null +++ b/dev/Sieve/Extensions/rfc5293.js @@ -0,0 +1,91 @@ +/** + * https://tools.ietf.org/html/rfc5293 + */ + +import { + GrammarCommand, + GrammarNumber, + GrammarQuotedString, + GrammarString, + GrammarStringList +} from 'Sieve/Grammar'; + +export class AddHeaderCommand extends GrammarCommand +{ + constructor() + { + super(); + this.last = false; + this.field_name = new GrammarQuotedString; + this.value = new GrammarQuotedString; + } + + get require() { return 'editheader'; } + + toString() + { + return this.identifier + + (this.last ? ' :last' : '') + + ' ' + this.field_name + + ' ' + this.value + ';'; + } + + pushArguments(args) + { + this.last = args.includes(':last'); + this.field_name = args[args.length - 2]; + this.value = args[args.length - 1]; + } +} + +export class DeleteHeaderCommand extends GrammarCommand +{ + constructor() + { + super(); + this.index = new GrammarNumber; + this.last = false; + this.comparator = '', + this.match_type = ':is', + this.field_name = new GrammarQuotedString; + this.value_patterns = new GrammarStringList; + } + + get require() { return 'editheader'; } + + toString() + { + return this.identifier + + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.field_name + + ' ' + this.value_patterns + ';'; + } + + pushArguments(args) + { + let l = args.length - 1; + args.forEach((arg, i) => { + if (':last' === arg) { + this.last = true; + } else if (':index' === args[i-1]) { + this.index.value = arg.value; + args[i] = null; + } + }); + + if (args[l-1] instanceof GrammarString) { + this.field_name = args[l-1]; + this.value_patterns = args[l]; + } else { + this.field_name = args[l]; + } + } +} +/* +Object.assign(Sieve.Commands, { + addheader: AddHeaderCommand, + deleteheader: DeleteHeaderCommand +}); +*/ diff --git a/dev/Sieve/Extensions/rfc5429.js b/dev/Sieve/Extensions/rfc5429.js new file mode 100644 index 000000000..b4bca755a --- /dev/null +++ b/dev/Sieve/Extensions/rfc5429.js @@ -0,0 +1,81 @@ +/** + * https://tools.ietf.org/html/rfc5429 + */ + +import { + GrammarCommand, + GrammarQuotedString, + GrammarString +} from 'Sieve/Grammar'; + +/** + * https://tools.ietf.org/html/rfc5429#section-2.1 + */ +export class ErejectCommand extends GrammarCommand +{ + constructor() + { + super(); + this._reason = new GrammarQuotedString; + } + + get require() { return 'ereject'; } + + toString() + { + return 'ereject ' + this._reason + ';'; + } + + get reason() + { + return this._reason.value; + } + + set reason(value) + { + this._reason.value = value; + } + + pushArguments(args) + { + if (args[0] instanceof GrammarString) { + this._reason = args[0]; + } + } +} + +/** + * https://tools.ietf.org/html/rfc5429#section-2.2 + */ +export class RejectCommand extends GrammarCommand +{ + constructor() + { + super(); + this._reason = new GrammarQuotedString; + } + + get require() { return 'reject'; } + + toString() + { + return 'reject ' + this._reason + ';'; + } + + get reason() + { + return this._reason.value; + } + + set reason(value) + { + this._reason.value = value; + } + + pushArguments(args) + { + if (args[0] instanceof GrammarString) { + this._reason = args[0]; + } + } +} diff --git a/dev/Sieve/Extensions/rfc6609.js b/dev/Sieve/Extensions/rfc6609.js new file mode 100644 index 000000000..35c8c6fc6 --- /dev/null +++ b/dev/Sieve/Extensions/rfc6609.js @@ -0,0 +1,47 @@ +/** + * https://tools.ietf.org/html/rfc6609 + */ + +import { + GrammarCommand, + GrammarQuotedString +} from 'Sieve/Grammar'; + +export class IncludeCommand extends GrammarCommand +{ + constructor() + { + super(); + this.global = false; // ':personal' / ':global'; + this.once = false; + this.optional = false; + this.value = new GrammarQuotedString; + } + + get require() { return 'include'; } + + toString() + { + return this.identifier + + (this.global ? ' :global' : '') + + (this.once ? ' :once' : '') + + (this.optional ? ' :optional' : '') + + ' ' + this.value + ';'; + } + + pushArguments(args) + { + args.forEach(arg => { + if (':global' === arg || ':once' === arg || ':optional' === arg) { + this[arg.substr(1)] = true; + } else if (arg instanceof GrammarQuotedString) { + this.value = arg; + } + }); + } +} + +export class ReturnCommand extends GrammarCommand +{ + get require() { return 'include'; } +} diff --git a/dev/Sieve/Grammar.js b/dev/Sieve/Grammar.js new file mode 100644 index 000000000..93381926c --- /dev/null +++ b/dev/Sieve/Grammar.js @@ -0,0 +1,277 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-8.2 + */ + +import { + QUOTED_TEXT, + HASH_COMMENT, + MULTILINE_LITERAL, + MULTILINE_DOTSTART +} from 'Sieve/RegEx'; + +import { + arrayToString +} from 'Sieve/Utils'; + +/** + * abstract + */ +export class GrammarString /*extends String*/ +{ + constructor(value = '') + { + this._value = value; + } + + toString() + { + return this._value; + } + + get value() + { + return this._value; + } + + set value(value) + { + this._value = value; + } + + get length() + { + return this._value.length; + } +} + +/** + * abstract + */ +export class GrammarComment extends GrammarString +{ +} + +/** + * https://tools.ietf.org/html/rfc5228#section-2.9 + */ +export class GrammarCommand +{ + constructor(identifier) + { + this.identifier = identifier || this.constructor.name.toLowerCase().replace('command', ''); + this.arguments = []; + this.commands = new GrammarCommands; + } + + toString() + { + let result = this.identifier; + if (this.arguments.length) { + result += ' ' + arrayToString(this.arguments, ' '); + } + return result + ( + this.commands.length ? ' ' + this.commands.toString() : ';' + ); + } + + getComparators() + { + return ['i;ascii-casemap']; + } + + getMatchTypes() + { + return [':is', ':contains', ':matches']; + } + + pushArguments(args) + { + this.arguments = args; + } +} + +export class GrammarCommands extends Array +{ + toString() + { + return this.length + ? '{\r\n\t' + arrayToString(this, '\r\n\t') + '\r\n}' + : '{}'; + } + + push(value) + { + if (value instanceof GrammarCommand || value instanceof GrammarComment) { + super.push(value); + } + } +} + +export class GrammarBracketComment extends GrammarComment +{ + toString() + { + return '/* ' + super.toString() + ' */'; + } +} + +export class GrammarHashComment extends GrammarComment +{ + toString() + { + return '# ' + super.toString(); + } +} + +export class GrammarNumber /*extends Number*/ +{ + constructor(value = '0') + { + this._value = value; + } + + toString() + { + return this._value; + } + + get value() + { + return this._value; + } + + set value(value) + { + this._value = value; + } +} + +export class GrammarStringList extends Array +{ + toString() + { + if (1 < this.length) { + return '[' + this.join(',') + ']'; + } + return this.length ? this[0] : ''; + } + + push(value) + { + if (!(value instanceof GrammarString)) { + value = new GrammarQuotedString(value); + } + super.push(value); + } +} + +const StringListRegEx = RegExp('(?:^\\s*|\\s*,\\s*)(?:"(' + QUOTED_TEXT + ')"|text:[ \\t]*(' + + HASH_COMMENT + ')?\\r\\n' + + '((?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*)' + + '\\.\\r\\n)', 'gm'); +GrammarStringList.fromString = list => { + let string, + obj = new GrammarStringList; + list = list.replace(/^[\r\n\t[]+/, ''); + while ((string = StringListRegEx.exec(list))) { + if (string[3]) { + obj.push(new GrammarMultiLine(string[3], string[2])); + } else { + obj.push(new GrammarQuotedString(string[1])); + } + } + return obj; +} + +export class GrammarQuotedString extends GrammarString +{ + toString() + { + return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"'; +// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-8.1 + */ +export class GrammarMultiLine extends GrammarString +{ + constructor(value, comment = '') + { + super(); + this.value = value; + this.comment = comment; + } + + toString() + { + return 'text:' + + (this.comment ? '# ' + this.comment : '') + "\r\n" + + this.value + + "\r\n.\r\n"; + } +} + +const MultiLineRegEx = RegExp('text:[ \\t]*(' + HASH_COMMENT + ')?\\r\\n' + + '((?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*)' + + '\\.\\r\\n', 'm'); +GrammarMultiLine.fromString = string => { + string = string.match(MultiLineRegEx); + if (string[2]) { + return new GrammarMultiLine(string[2].replace(/\r\n$/, ''), string[1]); + } + return new GrammarMultiLine(); +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5 + */ +export class GrammarTest +{ + constructor(identifier) + { + this.identifier = identifier || this.constructor.name.toLowerCase().replace('command', ''); + // Almost every test has a comparator and match_type, so define them here + this.comparator = '', + this.match_type = ':is', + this.arguments = []; + } + + toString() + { + return (this.identifier + + (this.comparator ? ' :comparator ' + this.comparator : '') + + (this.match_type ? ' ' + this.match_type : '') + + ' ' + arrayToString(this.arguments, ' ')).trim(); + } + + pushArguments(args) + { + this.arguments = args; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.2 + * https://tools.ietf.org/html/rfc5228#section-5.3 + */ +export class GrammarTestList extends Array +{ + toString() + { + if (1 < this.length) { +// return '(\r\n\t' + arrayToString(this, ',\r\n\t') + '\r\n)'; + return '(' + this.join(', ') + ')'; + } + return this.length ? this[0] : ''; + } + + push(value) + { + if (!(value instanceof GrammarTest)) { + throw 'Not an instanceof Test'; + } + super.push(value); + } +} diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js new file mode 100644 index 000000000..a558b5b66 --- /dev/null +++ b/dev/Sieve/Parser.js @@ -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; +}; diff --git a/dev/Sieve/README.md b/dev/Sieve/README.md new file mode 100644 index 000000000..ec8173089 --- /dev/null +++ b/dev/Sieve/README.md @@ -0,0 +1,33 @@ +https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml + +- [ ] RFC2852 envelope-deliverby / redirect-deliverby +- [ ] RFC3461 envelope-dsn / redirect-dsn +- [ ] RFC3894 copy +- [ ] RFC4790 comparator-* +- [x] RFC5173 body +- [x] RFC5183 environment +- [x] RFC5228 encoded-character / envelope / fileinto +- [x] RFC5229 variables +- [x] RFC5230 vacation +- [x] RFC5231 relational +- [x] RFC5232 imap4flags +- [x] RFC5233 subaddress +- [x] RFC5235 spamtest / spamtestplus / virustest +- [x] RFC5260 date / index +- [x] RFC5293 editheader +- [x] RFC5429 ereject / reject +- [ ] RFC5435 enotify +- [ ] RFC5463 ihave +- [ ] RFC5490 mailbox / mboxmetadata / servermetadata +- [ ] RFC5703 enclose / extracttext / foreverypart / mime / replace +- [ ] RFC6131 vacation-seconds +- [ ] RFC6134 extlists +- [ ] RFC6558 convert +- [x] RFC6609 include +- [ ] RFC6785 imapsieve +- [ ] RFC7352 duplicate +- [ ] RFC8579 special-use +- [ ] RFC8580 fcc +- [ ] RFC regex https://tools.ietf.org/html/draft-ietf-sieve-regex-01 +- [ ] vnd.cyrus.* +- [ ] vnd.dovecot.* diff --git a/dev/Sieve/RegEx.js b/dev/Sieve/RegEx.js new file mode 100644 index 000000000..6a883e0ca --- /dev/null +++ b/dev/Sieve/RegEx.js @@ -0,0 +1,203 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-8 + */ + +export 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"\\\\]', + + /** + * hash-comment = "#" *octet-not-crlf CRLF + */ + HASH_COMMENT = '#' + OCTET_NOT_CRLF + '*\\r\\n', + + /** + * QUANTIFIER = "K" / "M" / "G" + */ + QUANTIFIER = '[KMGkmg]', + + /** + * 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, + + /** + * 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 + ')*', + + /** + * 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 = '\\*', + + /** + * bracket-comment = "/*" *not-star 1*STAR *(not-star-slash *not-star 1*STAR) "/" + */ + BRACKET_COMMENT = '/\\*[\\s\\S]*?\\*/', + + /** + * identifier = (ALPHA / "_") *(ALPHA / DIGIT / "_") + */ + IDENTIFIER = '[a-zA-Z_][a-zA-Z0-9_]*', + + /** + * multi-line = "text:" *(SP / HTAB) (hash-comment / CRLF) + *(multiline-literal / multiline-dotstart) + "." CRLF + */ + MULTI_LINE = 'text:[ \\t]*(?:' + HASH_COMMENT + ')?\\r\\n' + + '(?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*' + + '\\.\\r\\n', + + /** + * 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_]*', + + /** + * comment = bracket-comment / hash-comment + */ +// COMMENT = BRACKET_COMMENT + '|' + HASH_COMMENT; + + /************************************************** + * https://tools.ietf.org/html/rfc5228#section-8.2 + **************************************************/ + + /** + * string = quoted-string / multi-line + */ + STRING = QUOTED_STRING + '|' + MULTI_LINE, + + /** + * string-list = "[" string *("," string) "]" / string + * if there is only a single string, the brackets are optional + */ + STRING_LIST = '\\[\\s*(?:' + STRING + ')(?:\\s*,\\s*(?:' + STRING + '))*\\s*\\]', +// + '|(?:' + STRING + ')', + + /** + * argument = string-list / number / tag + */ + ARGUMENT = STRING_LIST + '|' + STRING + '|' + NUMBER + '|' + 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 + **************************************************/ + + /** + * ADDRESS-PART = ":localpart" / ":domain" / ":all" + */ +// ADDRESS_PART = ':localpart|:domain|:all', + + /** + * COMPARATOR = ":comparator" string + */ +// COMPARATOR = ':comparator\\s+(?:' + STRING + ')'; + + /** + * MATCH-TYPE = ":is" / ":contains" / ":matches" + */ +// MATCH_TYPE = ':is|:contains|:matches' diff --git a/dev/Sieve/Tests.js b/dev/Sieve/Tests.js new file mode 100644 index 000000000..70c2e2487 --- /dev/null +++ b/dev/Sieve/Tests.js @@ -0,0 +1,288 @@ +/** + * https://tools.ietf.org/html/rfc5228#section-5 + */ + +import { + GrammarNumber, + GrammarString, + GrammarStringList, + GrammarTest, + GrammarTestList +} from 'Sieve/Grammar'; + +const + isAddressPart = tag => ':localpart' === tag || ':domain' === tag || ':all' === tag || isSubAddressPart(tag), + // https://tools.ietf.org/html/rfc5233 + isSubAddressPart = tag => ':user' === tag || ':detail' === tag; + +/** + * https://tools.ietf.org/html/rfc5228#section-5.1 + */ +export class AddressCommand extends GrammarTest +{ + constructor() + { + super(); + this.address_part = ':all'; + this.header_list = new GrammarStringList; + this.key_list = new GrammarStringList; + // rfc5260#section-6 +// this.index = new GrammarNumber; +// this.last = false; + } + + get require() { + let requires = []; + isSubAddressPart(this.address_part) && requires.push('subaddress'); + (this.last || (this.index && this.index.value)) && requires.push('index'); + return requires; + } + + toString() + { + return 'address' +// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.address_part + + ' ' + this.match_type + + ' ' + this.header_list.toString() + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (isAddressPart(arg)) { + this.address_part = arg; + } else if (':last' === arg) { + this.last = true; + } else if (':index' === args[i-1]) { + this.index.value = arg.value; + } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) { + this[args[i+1] ? 'header_list' : 'key_list'] = arg; +// (args[i+1] ? this.header_list : this.key_list) = arg; + } + }); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.2 + */ +export class AllOfCommand extends GrammarTest +{ + constructor() + { + super(); + this.tests = new GrammarTestList; + } + + toString() + { + return 'allof ' + this.tests.toString(); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.3 + */ +export class AnyOfCommand extends GrammarTest +{ + constructor() + { + super(); + this.tests = new GrammarTestList; + } + + toString() + { + return 'anyof ' + this.tests.toString(); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.4 + */ +export class EnvelopeCommand extends GrammarTest +{ + constructor() + { + super(); + this.address_part = ':all'; + this.envelope_part = new GrammarStringList; + this.key_list = new GrammarStringList; + } + + get require() { return isSubAddressPart(this.address_part) ? ['envelope','subaddress'] : 'envelope'; } + + toString() + { + return 'envelope' + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.address_part + + ' ' + this.match_type + + ' ' + this.envelope_part.toString() + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (isAddressPart(arg)) { + this.address_part = arg; + } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) { + this[args[i+1] ? 'envelope_part' : 'key_list'] = arg; +// (args[i+1] ? this.envelope_part : this.key_list) = arg; + } + }); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.5 + */ +export class ExistsCommand extends GrammarTest +{ + constructor() + { + super(); + this.header_names = new GrammarStringList; + } + + toString() + { + return 'exists ' + this.header_names.toString(); + } + + pushArguments(args) + { + if (args[0] instanceof GrammarStringList) { + this.header_names = args; + } else if (args[0] instanceof GrammarString) { + this.header_names.push(args[0].value); + } + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.6 + */ +export class FalseCommand extends GrammarTest +{ + toString() + { + return "false"; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.7 + */ +export class HeaderCommand extends GrammarTest +{ + constructor() + { + super(); + this.address_part = ':all'; + this.header_names = new GrammarStringList; + this.key_list = new GrammarStringList; + // rfc5260#section-6 +// this.index = new GrammarNumber; +// this.last = false; + } + + get require() { + let requires = []; + isSubAddressPart(this.address_part) && requires.push('subaddress'); + (this.last || (this.index && this.index.value)) && requires.push('index'); + return requires; + } + + toString() + { + return 'header' +// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + + (this.comparator ? ' :comparator ' + this.comparator : '') + + ' ' + this.match_type + + ' ' + this.header_names.toString() + + ' ' + this.key_list.toString(); + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (isAddressPart(arg)) { + this.address_part = arg; + } else if (':last' === arg) { + this.last = true; + } else if (':index' === args[i-1]) { + this.index.value = arg.value; + } else if (arg instanceof GrammarStringList || arg instanceof GrammarString) { + 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 + */ +export class NotCommand extends GrammarTest +{ + constructor() + { + super(); + this.test = new GrammarTest; + } + + toString() + { + return 'not ' + this.test; + } + + pushArguments() + { + throw 'No arguments'; + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.9 + */ +export class SizeCommand extends GrammarTest +{ + constructor() + { + super(); + 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 GrammarNumber) { + this.limit = arg; + } + }); + } +} + +/** + * https://tools.ietf.org/html/rfc5228#section-5.10 + */ +export class TrueCommand extends GrammarTest +{ + toString() + { + return 'true'; + } +} diff --git a/dev/Sieve/Utils.js b/dev/Sieve/Utils.js new file mode 100644 index 000000000..c458cc1fa --- /dev/null +++ b/dev/Sieve/Utils.js @@ -0,0 +1,4 @@ + +export const + arrayToString = (arr, separator) => + arr.map(item => item.toString ? item.toString() : item).join(separator); diff --git a/dev/View/Popup/SieveScript.js b/dev/View/Popup/SieveScript.js index 8dcb2882c..67dc8d829 100644 --- a/dev/View/Popup/SieveScript.js +++ b/dev/View/Popup/SieveScript.js @@ -12,6 +12,8 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { FilterPopupView } from 'View/Popup/Filter'; +//import { parseScript } from 'Sieve/Parser'; + export class SieveScriptPopupView extends AbstractViewPopup { constructor() { super('SieveScript'); @@ -122,6 +124,13 @@ export class SieveScriptPopupView extends AbstractViewPopup { this.rawActive(raw); this.allowToggle(!raw); this.saveError(false); + +/* + // TODO: Sieve GUI + let tree = parseScript(oScript.body(), oScript.name()); + console.dir(tree); + console.log(tree.join('\r\n')); +*/ } afterShow() { diff --git a/tasks/config.js b/tasks/config.js index 34a255a02..aab20f878 100644 --- a/tasks/config.js +++ b/tasks/config.js @@ -73,6 +73,9 @@ config.paths.js = { 'dev/External/SquireUI.js' ] }, + sieve: { + name: 'sieve.js' + }, app: { name: 'app.js' }, diff --git a/tasks/js.js b/tasks/js.js index d994c65b3..f1f565e87 100644 --- a/tasks/js.js +++ b/tasks/js.js @@ -53,6 +53,15 @@ const jsLibs = () => { .pipe(gulp.dest(config.paths.staticJS)); }; +// sieve +const jsSieve = async () => + (await rollupJS(config.paths.js.sieve.name)) +// .pipe(sourcemaps.write('.')) + .pipe(header(getHead() + '\n')) + .pipe(eol('\n', true)) + .pipe(gulp.dest(config.paths.staticJS)) + .on('error', gutil.log); + // app const jsApp = async () => (await rollupJS(config.paths.js.app.name)) @@ -124,6 +133,6 @@ exports.jsLint = jsLint; exports.js = gulp.series( jsClean, jsLint, - gulp.parallel(jsBoot, jsServiceWorker, jsOpenPGP, jsLibs, jsApp, jsAdmin), + gulp.parallel(jsBoot, jsServiceWorker, jsOpenPGP, jsLibs, jsSieve, jsApp, jsAdmin), jsMin );