From 63de537f8fdd50bca0e9ccd609fe20d500348a94 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 16 Mar 2022 12:05:50 +0100 Subject: [PATCH] Added Sieve extension rfc5703 --- dev/Sieve/Commands.js | 5 + dev/Sieve/Extensions/rfc5230.js | 3 +- dev/Sieve/Extensions/rfc5703.js | 178 ++++++++++++++++++++++++++++++++ dev/Sieve/Parser.js | 121 +++++++++++----------- dev/Sieve/Tests.js | 114 ++++++++++++++++---- 5 files changed, 341 insertions(+), 80 deletions(-) create mode 100644 dev/Sieve/Extensions/rfc5703.js diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js index 4f701d45d..79bf31849 100644 --- a/dev/Sieve/Commands.js +++ b/dev/Sieve/Commands.js @@ -33,6 +33,11 @@ export class ConditionalCommand extends GrammarCommand /* public function pushArguments(array $args): void { + args.forEach((arg, i) => { + if (':' === args[i-1][0]) { + this[args[i-1].replace(':','_')].value = arg.value; + } + }); print_r($args); exit; } diff --git a/dev/Sieve/Extensions/rfc5230.js b/dev/Sieve/Extensions/rfc5230.js index 85da557e9..1b077eb5c 100644 --- a/dev/Sieve/Extensions/rfc5230.js +++ b/dev/Sieve/Extensions/rfc5230.js @@ -42,7 +42,8 @@ export class VacationCommand extends GrammarCommand result += ' :subject ' + this._subject; } if (this._from.length) { - result += ' :from ' + this.arguments[':from']; + result += ' :from ' + this._from; +// result += ' :from ' + this.arguments[':from']; } if (this.addresses.length) { result += ' :addresses ' + this.addresses.toString(); diff --git a/dev/Sieve/Extensions/rfc5703.js b/dev/Sieve/Extensions/rfc5703.js new file mode 100644 index 000000000..096fd15e8 --- /dev/null +++ b/dev/Sieve/Extensions/rfc5703.js @@ -0,0 +1,178 @@ +/** + * https://tools.ietf.org/html/rfc5703 + */ + +import { + GrammarCommand, + GrammarNumber, + GrammarQuotedString, + GrammarString, + GrammarStringList +} from 'Sieve/Grammar'; + +/** + * https://datatracker.ietf.org/doc/html/rfc5703#section-3 + */ +export class ForEveryPartCommand extends GrammarCommand +{ + constructor() + { + super(); + this._name = new GrammarString; + } + + get require() { return 'foreverypart'; } + + toString() + { + let result = 'foreverypart'; + if (this._subject.length) { + result += ' :name ' + this._name; + } + return result + ' ' + this.commands; + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':name' === arg) { + this._name.value = args[i+1].value; + } + }); + } +} + +export class BreakCommand extends ForEveryPartCommand +{ + toString() + { + let result = 'break'; + if (this._subject.length) { + result += ' :name ' + this._name; + } + return result + ';'; + } +} + +/** + * https://datatracker.ietf.org/doc/html/rfc5703#section-5 + */ +export class ReplaceCommand extends GrammarCommand +{ + constructor() + { + super(); + this.mime = false; + this._subject = new GrammarQuotedString; + this._from = new GrammarQuotedString; + this.replacement = new GrammarQuotedString; + } + + get require() { return 'replace'; } + + toString() + { + let result = 'replace'; + if (this.mime) { + result += ' :mime'; + } + if (this._subject.length) { + result += ' :subject ' + this._subject; + } + if (this._from.length) { + result += ' :from ' + this._from; +// result += ' :from ' + this.arguments[':from']; + } + return result + this.replacement + ';'; + } + + pushArguments(args) + { + this.replacement = args.pop(); + args.forEach((arg, i) => { + if (':mime' === arg) { + this.mime = true; + } else if (':' === args[i-1][0]) { + // :subject, :from + this[args[i-1].replace(':','_')].value = arg.value; + } + }); + } +} + +/** + * https://datatracker.ietf.org/doc/html/rfc5703#section-6 + */ +export class EncloseCommand extends GrammarCommand +{ + constructor() + { + super(); + this._subject = new GrammarQuotedString; + this.headers = new GrammarStringList; + } + + get require() { return 'enclose'; } + + toString() + { + let result = 'enclose'; + if (this._subject.length) { + result += ' :subject ' + this._subject; + } + if (this.headers.length) { + result += ' :headers ' + this.headers; + } + return result + ' :text;'; + } + + pushArguments(args) + { + args.forEach((arg, i) => { + if (':' === args[i-1][0]) { + // :subject, :headers + this[args[i-1].replace(':','_')].value = arg.value; + } + }); + } +} + +/** + * https://datatracker.ietf.org/doc/html/rfc5703#section-7 + */ +export class ExtractTextCommand extends GrammarCommand +{ + constructor() + { + super(); + this.modifiers = []; + this._first = new GrammarNumber; + this.varname = new GrammarQuotedString; + } + + get require() { return 'extracttext'; } + + toString() + { + let result = 'extracttext ' + + this.modifiers.join(' '); + if (0 < this._first.value) { + result += ' :first ' + this._first; + } + return result + ' ' + this.varname + ';'; + } + + pushArguments(args) + { + this.varname = args.pop(); + [':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => { + args.includes(modifier) && this.modifiers.push(modifier); + }); + args.forEach((arg, i) => { + if (':' === args[i-1][0]) { + // :first + this[args[i-1].replace(':','_')].value = arg.value; + } + }); + } +} diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js index cbc2587a7..f64f037ae 100644 --- a/dev/Sieve/Parser.js +++ b/dev/Sieve/Parser.js @@ -2,7 +2,7 @@ * https://tools.ietf.org/html/rfc5228#section-8 */ -import { capa, forEachObjectEntry } from 'Sieve/Utils'; +import { capa } from 'Sieve/Utils'; import { BRACKET_COMMENT, @@ -57,14 +57,7 @@ import { BodyTest } from 'Sieve/Extensions/rfc5173'; import { EnvironmentTest } from 'Sieve/Extensions/rfc5183'; import { SetCommand, StringTest } from 'Sieve/Extensions/rfc5229'; import { VacationCommand } from 'Sieve/Extensions/rfc5230'; - -import { - SetFlagCommand, - AddFlagCommand, - RemoveFlagCommand, - HasFlagTest -} from 'Sieve/Extensions/rfc5232'; - +import { SetFlagCommand, AddFlagCommand, RemoveFlagCommand, HasFlagTest } from 'Sieve/Extensions/rfc5232'; import { SpamTestTest, VirusTestTest } from 'Sieve/Extensions/rfc5235'; import { DateTest, CurrentDateTest } from 'Sieve/Extensions/rfc5260'; import { AddHeaderCommand, DeleteHeaderCommand } from 'Sieve/Extensions/rfc5293'; @@ -72,74 +65,81 @@ import { ErejectCommand, RejectCommand } from 'Sieve/Extensions/rfc5429'; import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from 'Sieve/Extensions/rfc5435'; import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463'; import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490'; +import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703'; import { IncludeCommand, ReturnCommand } from 'Sieve/Extensions/rfc6609'; const - AllCommands = { + AllCommands = [ // Control commands - if: IfCommand, - elsif: ElsIfCommand, - else: ElseCommand, - conditional: ConditionalCommand, - require: RequireCommand, - stop: StopCommand, + IfCommand, + ElsIfCommand, + ElseCommand, + ConditionalCommand, + RequireCommand, + StopCommand, // Action commands - discard: DiscardCommand, - fileinto: FileIntoCommand, - keep: KeepCommand, - redirect: RedirectCommand, + DiscardCommand, + FileIntoCommand, + KeepCommand, + RedirectCommand, // Test commands - address: AddressTest, - allof: AllOfTest, - anyof: AnyOfTest, - envelope: EnvelopeTest, - exists: ExistsTest, - false: FalseTest, - header: HeaderTest, - not: NotTest, - size: SizeTest, - true: TrueTest, + AddressTest, + AllOfTest, + AnyOfTest, + EnvelopeTest, + ExistsTest, + FalseTest, + HeaderTest, + NotTest, + SizeTest, + TrueTest, // rfc5173 - body: BodyTest, + BodyTest, // rfc5183 - environment: EnvironmentTest, + EnvironmentTest, // rfc5229 - set: SetCommand, - string: StringTest, + SetCommand, + StringTest, // rfc5230 - vacation: VacationCommand, + VacationCommand, // rfc5232 - setflag: SetFlagCommand, - addflag: AddFlagCommand, - removeflag: RemoveFlagCommand, - hasflag: HasFlagTest, + SetFlagCommand, + AddFlagCommand, + RemoveFlagCommand, + HasFlagTest, // rfc5235 - spamtest: SpamTestTest, - virustest: VirusTestTest, + SpamTestTest, + VirusTestTest, // rfc5260 - date: DateTest, - currentdate: CurrentDateTest, + DateTest, + CurrentDateTest, // rfc5293 AddHeaderCommand, DeleteHeaderCommand, // rfc5429 - ereject: ErejectCommand, - reject: RejectCommand, + ErejectCommand, + RejectCommand, // rfc5435 - notify: NotifyCommand, - valid_notify_method: ValidNotifyMethodTest, - notify_method_capability: NotifyMethodCapabilityTest, + NotifyCommand, + ValidNotifyMethodTest, + NotifyMethodCapabilityTest, // rfc5463 - ihave: IHaveTest, - error: ErrorCommand, + IHaveTest, + ErrorCommand, // rfc5490 - mailboxexists: MailboxExistsTest, - metadata: MetadataTest, - metadataexists: MetadataExistsTest, + MailboxExistsTest, + MetadataTest, + MetadataExistsTest, + // rfc5703 + ForEveryPartCommand, + BreakCommand, + ReplaceCommand, + EncloseCommand, + ExtractTextCommand, // rfc6609 - include: IncludeCommand, - return: ReturnCommand - }, + IncludeCommand, + ReturnCommand + ], T_UNKNOWN = 0, T_STRING_LIST = 1, @@ -182,12 +182,12 @@ export const parseScript = (script, name = 'script.sieve') => { // Only activate available commands const Commands = {}; - forEachObjectEntry(AllCommands, (key, cmd) => { - const requires = (new cmd).require; + AllCommands.forEach(cmd => { + const obj = new cmd, requires = obj.require; if (!requires || (Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string)) ) { - Commands[key] = cmd; + Commands[obj.identifier] = cmd; } }); @@ -260,13 +260,14 @@ export const parseScript = (script, name = 'script.sieve') => { } new_command = new Commands[value](); } else { - console.error('Unknown command: ' + value); if (command && ( command instanceof ConditionalCommand || command instanceof NotTest || command.tests instanceof GrammarTestList)) { + console.error('Unknown test: ' + value); new_command = new GrammarTest(value); } else { + console.error('Unknown command: ' + value); new_command = new GrammarCommand(value); } } diff --git a/dev/Sieve/Tests.js b/dev/Sieve/Tests.js index 30949f0ed..dbd35837f 100644 --- a/dev/Sieve/Tests.js +++ b/dev/Sieve/Tests.js @@ -2,6 +2,8 @@ * https://tools.ietf.org/html/rfc5228#section-5 */ +import { capa } from 'Sieve/Utils'; + import { GrammarNumber, GrammarString, @@ -13,7 +15,18 @@ import { const isAddressPart = tag => ':localpart' === tag || ':domain' === tag || ':all' === tag || isSubAddressPart(tag), // https://tools.ietf.org/html/rfc5233 - isSubAddressPart = tag => ':user' === tag || ':detail' === tag; + isSubAddressPart = tag => ':user' === tag || ':detail' === tag, + + asStringList = arg => { + if (arg instanceof GrammarStringList) { + return arg; + } + let args = new GrammarStringList(); + if (arg instanceof GrammarString) { + args.push(arg.value); + } + return args; + }; /** * https://tools.ietf.org/html/rfc5228#section-5.1 @@ -29,18 +42,31 @@ export class AddressTest extends GrammarTest // rfc5260#section-6 // this.index = new GrammarNumber; // this.last = false; + // rfc5703#section-6 +// this.mime +// this.anychild } get require() { let requires = []; isSubAddressPart(this.address_part) && requires.push('subaddress'); (this.last || (this.index && this.index.value)) && requires.push('index'); + (this.mime || this.anychild) && requires.push('mime'); return requires; } toString() { - return 'address' + let result = 'address'; + if (capa.includes('mime')) { + if (this.mime) { + result += ' :mime'; + } + if (this.anychild) { + result += ' :anychild'; + } + } + return result // + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + (this.comparator ? ' :comparator ' + this.comparator : '') + ' ' + this.address_part @@ -51,16 +77,19 @@ export class AddressTest extends GrammarTest pushArguments(args) { + this.key_list = asStringList(args.pop()); + this.header_list = asStringList(args.pop()); args.forEach((arg, i) => { if (isAddressPart(arg)) { this.address_part = arg; } else if (':last' === arg) { this.last = true; + } else if (':mime' === arg) { + this.mime = true; + } else if (':anychild' === arg) { + this.anychild = 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; } }); } @@ -127,12 +156,11 @@ export class EnvelopeTest extends GrammarTest pushArguments(args) { - args.forEach((arg, i) => { + this.key_list = asStringList(args.pop()); + this.envelope_part = asStringList(args.pop()); + args.forEach(arg => { 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; } }); } @@ -147,20 +175,39 @@ export class ExistsTest extends GrammarTest { super(); this.header_names = new GrammarStringList; + // rfc5703#section-6 +// this.mime +// this.anychild + } + + get require() { + return (this.mime || this.anychild) ? ['mime'] : null; } toString() { - return 'exists ' + this.header_names.toString(); + let result = 'exists'; + if (capa.includes('mime')) { + if (this.mime) { + result += ' :mime'; + } + if (this.anychild) { + result += ' :anychild'; + } + } + return result + ' ' + 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); - } + this.header_names = asStringList(args.pop()); + args.forEach(arg => { + if (':mime' === arg) { + this.mime = true; + } else if (':anychild' === arg) { + this.mime = true; + } + }); } } @@ -189,18 +236,48 @@ export class HeaderTest extends GrammarTest // rfc5260#section-6 // this.index = new GrammarNumber; // this.last = false; + // rfc5703#section-6 + this.mime = false; + this.anychild = false; + // when ":mime" is used: + this.type = false; + this.subtype = false; + this.contenttype = false; + this.param = new GrammarStringList; } get require() { let requires = []; isSubAddressPart(this.address_part) && requires.push('subaddress'); (this.last || (this.index && this.index.value)) && requires.push('index'); + (this.mime || this.anychild) && requires.push('mime'); return requires; } toString() { - return 'header' + let result = 'header'; + if (capa.includes('mime')) { + if (this.mime) { + result += ' :mime'; + if (this.type) { + result += ' :type'; + } + if (this.subtype) { + result += ' :subtype'; + } + if (this.contenttype) { + result += ' :contenttype'; + } + if (this.param.length) { + result += ' :param ' + this.param; + } + } + if (this.anychild) { + result += ' :anychild'; + } + } + return result // + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + (this.comparator ? ' :comparator ' + this.comparator : '') + ' ' + this.match_type @@ -210,6 +287,8 @@ export class HeaderTest extends GrammarTest pushArguments(args) { + this.key_list = asStringList(args.pop()); + this.header_names = asStringList(args.pop()); args.forEach((arg, i) => { if (isAddressPart(arg)) { this.address_part = arg; @@ -217,9 +296,6 @@ export class HeaderTest extends GrammarTest 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; } }); }