Simplify Sieve Parser and added RFC5235

This commit is contained in:
djmaze 2021-01-14 23:42:46 +01:00
parent 22964f1fde
commit d9865e3a46
11 changed files with 308 additions and 92 deletions

View file

@ -23,9 +23,7 @@ class Conditional extends Command
toString() toString()
{ {
return this.identifier return this.identifier + ' ' + this.test + ' ' + this.commands;
+ ('else' !== this.identifier ? ' ' + this.test : '')
+ ' ' + this.commands;
} }
/* /*
public function pushArguments(array $args): void 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 * https://tools.ietf.org/html/rfc5228#section-3.2
*/ */
@ -83,13 +110,12 @@ class Stop extends Command
*/ */
class FileInto extends Command class FileInto extends Command
{ {
// const REQUIRE = 'fileinto';
constructor() constructor()
{ {
super('fileinto'); super('fileinto');
// QuotedString / MultiLine // QuotedString / MultiLine
this._mailbox = new Grammar.QuotedString(); this._mailbox = new Grammar.QuotedString();
// this.require = 'fileinto';
} }
toString() toString()
@ -177,14 +203,17 @@ class Discard extends Command
Sieve.Commands = { Sieve.Commands = {
// Control commands // Control commands
Conditional: Conditional, if: If,
Require: Require, elsif: ElsIf,
Stop: Stop, else: Else,
conditional: Conditional,
require: Require,
stop: Stop,
// Action commands // Action commands
Discard: Discard, discard: Discard,
Fileinto: FileInto, fileinto: FileInto,
Keep: Keep, keep: Keep,
Redirect: Redirect redirect: Redirect
}; };
})(this.Sieve); })(this.Sieve);

View file

@ -1,17 +0,0 @@
/**
* https://tools.ietf.org/html/rfc5232
*/
(Sieve => {
Sieve.Extensions.Imap4flags = class
{
/**
* setflag [<variablename: string>] <list-of-flags: string-list>
* addflag [<variablename: string>] <list-of-flags: string-list>
* removeflag [<variablename: string>] <list-of-flags: string-list>
* hasflag [MATCH-TYPE] [COMPARATOR] [<variable-list: string-list>] <list-of-flags: string-list>
*/
};
})(this.Sieve);

View file

@ -15,6 +15,7 @@ class Body extends Grammar.Test
this.match_type = ':is', this.match_type = ':is',
this.body_transform = ''; // :raw, :content <string-list>, :text this.body_transform = ''; // :raw, :content <string-list>, :text
this.key_list = new Grammar.StringList; this.key_list = new Grammar.StringList;
// this.require = 'body';
} }
toString() toString()
@ -26,7 +27,6 @@ class Body extends Grammar.Test
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
args.forEach((arg, i) => { args.forEach((arg, i) => {
@ -43,7 +43,7 @@ class Body extends Grammar.Test
} }
} }
Sieve.Extensions.Body = Body; Sieve.Commands.body = Body;
})(this.Sieve); })(this.Sieve);

View file

@ -4,20 +4,23 @@
(Sieve => { (Sieve => {
class Vacation extends Sieve.Grammar.Command const Grammar = Sieve.Grammar;
class Vacation extends Grammar.Command
{ {
constructor() constructor()
{ {
super('vacation'); super('vacation');
this.arguments = { this.arguments = {
':days' : new Sieve.Grammar.Number, ':days' : new Grammar.Number,
':subject' : new Sieve.Grammar.QuotedString, ':subject' : new Grammar.QuotedString,
':from' : new Sieve.Grammar.QuotedString, ':from' : new Grammar.QuotedString,
':addresses': new Sieve.Grammar.StringList, ':addresses': new Grammar.StringList,
':mime' : false, ':mime' : false,
':handle' : new Sieve.Grammar.QuotedString ':handle' : new Grammar.QuotedString
}; };
this.reason = ''; // QuotedString / MultiLine this.reason = ''; // QuotedString / MultiLine
// this.require = 'vacation';
} }
toString() toString()
@ -82,6 +85,6 @@ class Vacation extends Sieve.Grammar.Command
*/ */
} }
Sieve.Extensions.Vacation = Vacation; Sieve.Commands.vacation = Vacation;
})(this.Sieve); })(this.Sieve);

View file

@ -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);

View file

@ -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);

View file

@ -15,6 +15,7 @@ class Ereject extends Grammar.Command
{ {
super('ereject'); super('ereject');
this._reason = new Grammar.QuotedString; this._reason = new Grammar.QuotedString;
// this.require = 'ereject';
} }
toString() toString()
@ -49,6 +50,7 @@ class Reject extends Grammar.Command
{ {
super('reject'); super('reject');
this._reason = new Grammar.QuotedString; this._reason = new Grammar.QuotedString;
// this.require = 'reject';
} }
toString() toString()
@ -74,7 +76,7 @@ class Reject extends Grammar.Command
} }
} }
Sieve.Extensions.Ereject = Ereject; Sieve.Commands.ereject = Ereject;
Sieve.Extensions.Reject = Reject; Sieve.Commands.reject = Reject;
})(this.Sieve); })(this.Sieve);

View file

@ -6,6 +6,8 @@
const const
RegEx = Sieve.RegEx, RegEx = Sieve.RegEx,
Grammar = Sieve.Grammar,
Commands = Sieve.Commands,
T_UNKNOWN = 0, T_UNKNOWN = 0,
T_STRING_LIST = 1, T_STRING_LIST = 1,
@ -80,43 +82,35 @@ Sieve.parseScript = script => {
case T_IDENTIFIER: { case T_IDENTIFIER: {
pushArgs(); pushArgs();
value = value.toLowerCase(); value = value.toLowerCase();
let new_command, let new_command;
className = value[0].toUpperCase() + value.substring(1);
if ('if' === value) { if ('if' === value) {
new_command = new Sieve.Commands.Conditional(value); new_command = new Commands.conditional(value);
} else if ('elsif' === value || 'else' === value) { } else if ('elsif' === value || 'else' === value) {
// (prev_command instanceof Sieve.Commands.Conditional) || error('Not after IF condition'); // (prev_command instanceof Commands.conditional) || error('Not after IF condition');
new_command = new Sieve.Commands.Conditional(value); new_command = new Commands.conditional(value);
} else if ('allof' === value || 'anyof' === value) { } else if (Commands[value]) {
(command instanceof Sieve.Commands.Conditional) || error('Test-list not in conditional'); if ('allof' === value || 'anyof' === value) {
new_command = new Sieve.Tests[className](); (command instanceof Commands.conditional) || error('Test-list not in conditional');
} else if (Sieve.Tests[className]) { }
// address / envelope / exists / header / not / size new_command = new Commands[value]();
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]();
} else { } else {
console.error('Unknown command: ' + value); console.error('Unknown command: ' + value);
if (command && ( if (command && (
command instanceof Sieve.Commands.Conditional command instanceof Commands.conditional
|| command instanceof Sieve.Tests.Not || command instanceof Commands.not
|| command.tests instanceof Sieve.Grammar.TestList)) { || command.tests instanceof Grammar.TestList)) {
new_command = new Sieve.Grammar.Test(value); new_command = new Grammar.Test(value);
} else { } else {
new_command = new Sieve.Grammar.Command(value); new_command = new Grammar.Command(value);
} }
} }
if (new_command instanceof Sieve.Grammar.Test) { if (new_command instanceof Grammar.Test) {
if (command instanceof Sieve.Commands.Conditional || command instanceof Sieve.Tests.Not) { if (command instanceof Commands.conditional || command instanceof Commands.not) {
// if/elsif/else new_command // if/elsif/else new_command
// not new_command // not new_command
command.test = 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 // allof/anyof .tests[] new_command
command.tests.push(new_command); command.tests.push(new_command);
} else { } else {
@ -144,35 +138,35 @@ Sieve.parseScript = script => {
break; break;
case T_STRING_LIST: case T_STRING_LIST:
command command
? args.push(Sieve.Grammar.StringList.fromString(value)) ? args.push(Grammar.StringList.fromString(value))
: error('String list must be command argument'); : error('String list must be command argument');
break; break;
case T_MULTILINE_STRING: case T_MULTILINE_STRING:
command command
? args.push(new Sieve.Grammar.MultiLine(value)) ? args.push(new Grammar.MultiLine(value))
: error('Multi-line string must be command argument'); : error('Multi-line string must be command argument');
break; break;
case T_QUOTED_STRING: case T_QUOTED_STRING:
command 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'); : error('Quoted string must be command argument');
break; break;
case T_NUMBER: case T_NUMBER:
command command
? args.push(new Sieve.Grammar.Number(value)) ? args.push(new Grammar.Number(value))
: error('Number must be command argument'); : error('Number must be command argument');
break; break;
// Comments // Comments
case T_BRACKET_COMMENT: case T_BRACKET_COMMENT:
(command ? command.commands : tree).push( (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; break;
case T_HASH_COMMENT: case T_HASH_COMMENT:
(command ? command.commands : tree).push( (command ? command.commands : tree).push(
new Sieve.Grammar.HashComment(value.substr(1).trim()) new Grammar.HashComment(value.substr(1).trim())
); );
break; break;
@ -194,14 +188,14 @@ Sieve.parseScript = script => {
pushArgs(); pushArgs();
// https://tools.ietf.org/html/rfc5228#section-2.9 // https://tools.ietf.org/html/rfc5228#section-2.9
// Action commands do not take tests or blocks // Action commands do not take tests or blocks
while (command && !(command instanceof Sieve.Commands.Conditional)) { while (command && !(command instanceof Commands.conditional)) {
levels.pop(); levels.pop();
command = levels.last(); command = levels.last();
} }
command || error('Block start not part of control command'); command || error('Block start not part of control command');
break; break;
case T_BLOCK_END: 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(); levels.pop();
// prev_command = command; // prev_command = command;
command = levels.last(); command = levels.last();
@ -210,7 +204,7 @@ Sieve.parseScript = script => {
// anyof / allof ( ... , ... ) // anyof / allof ( ... , ... )
case T_LEFT_PARENTHESIS: case T_LEFT_PARENTHESIS:
pushArgs(); pushArgs();
while (command && !(command.tests instanceof Sieve.Grammar.TestList)) { while (command && !(command.tests instanceof Grammar.TestList)) {
levels.pop(); levels.pop();
command = levels.last(); command = levels.last();
} }
@ -220,14 +214,14 @@ Sieve.parseScript = script => {
pushArgs(); pushArgs();
levels.pop(); levels.pop();
command = levels.last(); 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; break;
case T_COMMA: case T_COMMA:
pushArgs(); pushArgs();
levels.pop(); levels.pop();
command = levels.last(); command = levels.last();
// Must be inside PARENTHESIS aka test-list // 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; break;
case T_UNKNOWN: case T_UNKNOWN:

View file

@ -236,17 +236,17 @@ class True extends Test
} }
} }
Sieve.Tests = { Object.assign(Sieve.Commands, {
Address: Address, address: Address,
Allof: AllOf, allof: AllOf,
Anyof: AnyOf, anyof: AnyOf,
Envelope: Envelope, envelope: Envelope,
Exists: Exists, exists: Exists,
False: False, false: False,
Header: Header, header: Header,
Not: Not, not: Not,
Size: Size, size: Size,
True: True true: True
}; });
})(this.Sieve); })(this.Sieve);

View file

@ -6,11 +6,8 @@
RegEx: {}, RegEx: {},
Grammar: {}, Grammar: {},
Commands: {}, Commands: {},
Tests: {},
parseScript: ()=>{}, parseScript: ()=>{},
*/ */
Extensions: {},
arrayToString: (arr, separator) => arrayToString: (arr, separator) =>
arr.map(item => item.toString ? item.toString() : item).join(separator) arr.map(item => item.toString ? item.toString() : item).join(separator)
}; };

View file

@ -57,7 +57,6 @@ const jsSieve = () => {
.pipe(expect.real({ errorOnFailure: true }, src)) .pipe(expect.real({ errorOnFailure: true }, src))
.pipe(concat(config.paths.js.sieve.name, { separator: '\n\n' })) .pipe(concat(config.paths.js.sieve.name, { separator: '\n\n' }))
.pipe(eol('\n', true)) .pipe(eol('\n', true))
.pipe(replace(/sourceMappingURL=[a-z0-9.\-_]{1,20}\.map/gi, ''))
.pipe(gulp.dest(config.paths.staticJS)); .pipe(gulp.dest(config.paths.staticJS));
}; };