mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-11 00:14:50 +03:00
Split Sieve/Filters code from app.js so that i can work on the new Sieve GUI
This commit is contained in:
parent
ec6a79d9d6
commit
d6dc4d291c
23 changed files with 460 additions and 490 deletions
121
dev/Sieve/Model/Abstract.js
Normal file
121
dev/Sieve/Model/Abstract.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { forEachObjectValue, forEachObjectEntry, koComputable } from 'Sieve/Utils';
|
||||
|
||||
function typeCast(curValue, newValue) {
|
||||
if (null != curValue) {
|
||||
switch (typeof curValue)
|
||||
{
|
||||
case 'boolean': return 0 != newValue && !!newValue;
|
||||
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
|
||||
case 'string': return null != newValue ? '' + newValue : '';
|
||||
case 'object':
|
||||
if (curValue.constructor.reviveFromJson) {
|
||||
return curValue.constructor.reviveFromJson(newValue);
|
||||
}
|
||||
if (Array.isArray(curValue) && !Array.isArray(newValue))
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
||||
export class AbstractModel {
|
||||
constructor() {
|
||||
/*
|
||||
if (new.target === AbstractModel) {
|
||||
throw new Error("Can't instantiate AbstractModel!");
|
||||
}
|
||||
*/
|
||||
this.disposables = [];
|
||||
}
|
||||
|
||||
addObservables(observables) {
|
||||
forEachObjectEntry(observables, (key, value) =>
|
||||
this[key] || (this[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value))
|
||||
);
|
||||
}
|
||||
|
||||
addComputables(computables) {
|
||||
forEachObjectEntry(computables, (key, fn) => this[key] = koComputable(fn));
|
||||
}
|
||||
|
||||
addSubscribables(subscribables) {
|
||||
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
|
||||
}
|
||||
|
||||
/** Called by delegateRunOnDestroy */
|
||||
onDestroy() {
|
||||
/** dispose ko subscribables */
|
||||
this.disposables.forEach(disposable => {
|
||||
disposable && typeof disposable.dispose === 'function' && disposable.dispose();
|
||||
});
|
||||
/** clear object entries */
|
||||
// forEachObjectEntry(this, (key, value) => {
|
||||
forEachObjectValue(this, value => {
|
||||
/** clear CollectionModel */
|
||||
let arr = ko.isObservableArray(value) ? value() : value;
|
||||
arr && arr.onDestroy && arr.onDestroy();
|
||||
/** destroy ko.observable/ko.computed? */
|
||||
// dispose(value);
|
||||
/** clear object value */
|
||||
// this[key] = null; // TODO: issue with Contacts view
|
||||
});
|
||||
// this.disposables = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJson} json
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static validJson(json) {
|
||||
return !!(json && ('Object/'+this.name.replace('Model', '') === json['@Object']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJson} json
|
||||
* @returns {*Model}
|
||||
*/
|
||||
static reviveFromJson(json) {
|
||||
let obj = this.validJson(json) ? new this() : null;
|
||||
obj && obj.revivePropertiesFromJson(json);
|
||||
return obj;
|
||||
}
|
||||
|
||||
revivePropertiesFromJson(json) {
|
||||
let model = this.constructor;
|
||||
if (!model.validJson(json)) {
|
||||
return false;
|
||||
}
|
||||
forEachObjectEntry(json, (key, value) => {
|
||||
if ('@' !== key[0]) try {
|
||||
key = key[0].toLowerCase() + key.slice(1);
|
||||
switch (typeof this[key])
|
||||
{
|
||||
case 'function':
|
||||
if (ko.isObservable(this[key])) {
|
||||
this[key](typeCast(this[key](), value));
|
||||
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
|
||||
}
|
||||
// else console.log(model.name + '.' + key + ' is a function');
|
||||
break;
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'object':
|
||||
case 'string':
|
||||
this[key] = typeCast(this[key], value);
|
||||
break;
|
||||
// fall through
|
||||
case 'undefined':
|
||||
default:
|
||||
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(model.name + '.' + key);
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
267
dev/Sieve/Model/Filter.js
Normal file
267
dev/Sieve/Model/Filter.js
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import { koArrayWithDestroy } from 'Sieve/Utils';
|
||||
|
||||
//import { getFolderFromCacheList } from 'Common/Cache';
|
||||
|
||||
//import { AccountUserStore } from 'Stores/User/Account';
|
||||
|
||||
import { FilterConditionModel } from 'Sieve/Model/FilterCondition';
|
||||
import { AbstractModel } from 'Sieve/Model/Abstract';
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FilterAction = {
|
||||
None: 'None',
|
||||
MoveTo: 'MoveTo',
|
||||
Discard: 'Discard',
|
||||
Vacation: 'Vacation',
|
||||
Reject: 'Reject',
|
||||
Forward: 'Forward'
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
const FilterRulesType = {
|
||||
All: 'All',
|
||||
Any: 'Any'
|
||||
};
|
||||
|
||||
export class FilterModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.id = '';
|
||||
|
||||
this.addObservables({
|
||||
enabled: true,
|
||||
askDelete: false,
|
||||
canBeDeleted: true,
|
||||
|
||||
name: '',
|
||||
nameError: false,
|
||||
nameFocused: false,
|
||||
|
||||
conditionsType: FilterRulesType.Any,
|
||||
|
||||
// Actions
|
||||
actionValue: '',
|
||||
actionValueError: false,
|
||||
|
||||
actionValueSecond: '',
|
||||
actionValueThird: '',
|
||||
|
||||
actionValueFourth: '',
|
||||
actionValueFourthError: false,
|
||||
|
||||
actionMarkAsRead: false,
|
||||
|
||||
actionKeep: true,
|
||||
actionNoStop: false,
|
||||
|
||||
actionType: FilterAction.MoveTo
|
||||
});
|
||||
|
||||
this.conditions = koArrayWithDestroy();
|
||||
|
||||
const fGetRealFolderName = folderFullName => {
|
||||
// const folder = getFolderFromCacheList(folderFullName);
|
||||
// return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName;
|
||||
return folderFullName;
|
||||
};
|
||||
|
||||
this.addComputables({
|
||||
nameSub: () => {
|
||||
let result = '';
|
||||
const actionValue = this.actionValue(), root = 'SETTINGS_FILTERS/SUBNAME_';
|
||||
|
||||
switch (this.actionType()) {
|
||||
case FilterAction.MoveTo:
|
||||
result = rl.i18n(root + 'MOVE_TO', {
|
||||
FOLDER: fGetRealFolderName(actionValue)
|
||||
});
|
||||
break;
|
||||
case FilterAction.Forward:
|
||||
result = rl.i18n(root + 'FORWARD_TO', {
|
||||
EMAIL: actionValue
|
||||
});
|
||||
break;
|
||||
case FilterAction.Vacation:
|
||||
result = rl.i18n(root + 'VACATION_MESSAGE');
|
||||
break;
|
||||
case FilterAction.Reject:
|
||||
result = rl.i18n(root + 'REJECT');
|
||||
break;
|
||||
case FilterAction.Discard:
|
||||
result = rl.i18n(root + 'DISCARD');
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
return result ? '(' + result + ')' : '';
|
||||
},
|
||||
|
||||
actionTemplate: () => {
|
||||
const result = 'SettingsFiltersAction';
|
||||
switch (this.actionType()) {
|
||||
case FilterAction.Forward:
|
||||
return result + 'Forward';
|
||||
case FilterAction.Vacation:
|
||||
return result + 'Vacation';
|
||||
case FilterAction.Reject:
|
||||
return result + 'Reject';
|
||||
case FilterAction.None:
|
||||
return result + 'None';
|
||||
case FilterAction.Discard:
|
||||
return result + 'Discard';
|
||||
case FilterAction.MoveTo:
|
||||
default:
|
||||
return result + 'MoveToFolder';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.addSubscribables({
|
||||
name: sValue => this.nameError(!sValue),
|
||||
actionValue: sValue => this.actionValueError(!sValue),
|
||||
actionType: () => {
|
||||
this.actionValue('');
|
||||
this.actionValueError(false);
|
||||
this.actionValueSecond('');
|
||||
this.actionValueThird('');
|
||||
this.actionValueFourth('');
|
||||
this.actionValueFourthError(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateID() {
|
||||
this.id = Jua.randomId();
|
||||
}
|
||||
|
||||
verify() {
|
||||
if (!this.name()) {
|
||||
this.nameError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.conditions.length && this.conditions.find(cond => cond && !cond.verify())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.actionValue()) {
|
||||
if ([
|
||||
FilterAction.MoveTo,
|
||||
FilterAction.Forward,
|
||||
FilterAction.Reject,
|
||||
FilterAction.Vacation
|
||||
].includes(this.actionType())
|
||||
) {
|
||||
this.actionValueError(true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (FilterAction.Forward === this.actionType() && !this.actionValue().includes('@')) {
|
||||
this.actionValueError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
FilterAction.Vacation === this.actionType() &&
|
||||
this.actionValueFourth() &&
|
||||
!this.actionValueFourth().includes('@')
|
||||
) {
|
||||
this.actionValueFourthError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.nameError(false);
|
||||
this.actionValueError(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
// '@Object': 'Object/Filter',
|
||||
ID: this.id,
|
||||
Enabled: this.enabled() ? 1 : 0,
|
||||
Name: this.name(),
|
||||
Conditions: this.conditions.map(item => item.toJson()),
|
||||
ConditionsType: this.conditionsType(),
|
||||
|
||||
ActionType: this.actionType(),
|
||||
ActionValue: this.actionValue(),
|
||||
ActionValueSecond: this.actionValueSecond(),
|
||||
ActionValueThird: this.actionValueThird(),
|
||||
ActionValueFourth: this.actionValueFourth(),
|
||||
|
||||
Keep: this.actionKeep() ? 1 : 0,
|
||||
Stop: this.actionNoStop() ? 0 : 1,
|
||||
MarkAsRead: this.actionMarkAsRead() ? 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
addCondition() {
|
||||
this.conditions.push(new FilterConditionModel());
|
||||
}
|
||||
|
||||
removeCondition(oConditionToDelete) {
|
||||
this.conditions.remove(oConditionToDelete);
|
||||
}
|
||||
|
||||
setRecipients() {
|
||||
// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJsonFilter} json
|
||||
* @returns {?FilterModel}
|
||||
*/
|
||||
static reviveFromJson(json) {
|
||||
const filter = super.reviveFromJson(json);
|
||||
if (filter) {
|
||||
filter.id = filter.id ? '' + filter.id : '';
|
||||
filter.conditions(
|
||||
json.Conditions ? json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) : []
|
||||
);
|
||||
filter.actionKeep(0 != json.Keep);
|
||||
filter.actionNoStop(0 == json.Stop);
|
||||
filter.actionMarkAsRead(1 == json.MarkAsRead);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
cloneSelf() {
|
||||
const filter = new FilterModel();
|
||||
|
||||
filter.id = this.id;
|
||||
|
||||
filter.enabled(this.enabled());
|
||||
|
||||
filter.name(this.name());
|
||||
filter.nameError(this.nameError());
|
||||
|
||||
filter.conditionsType(this.conditionsType());
|
||||
|
||||
filter.actionMarkAsRead(this.actionMarkAsRead());
|
||||
|
||||
filter.actionType(this.actionType());
|
||||
|
||||
filter.actionValue(this.actionValue());
|
||||
filter.actionValueError(this.actionValueError());
|
||||
|
||||
filter.actionValueSecond(this.actionValueSecond());
|
||||
filter.actionValueThird(this.actionValueThird());
|
||||
filter.actionValueFourth(this.actionValueFourth());
|
||||
|
||||
filter.actionKeep(this.actionKeep());
|
||||
filter.actionNoStop(this.actionNoStop());
|
||||
|
||||
filter.conditions(this.conditions.map(item => item.cloneSelf()));
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
104
dev/Sieve/Model/FilterCondition.js
Normal file
104
dev/Sieve/Model/FilterCondition.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { koComputable } from 'Sieve/Utils';
|
||||
|
||||
import { AbstractModel } from 'Sieve/Model/Abstract';
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FilterConditionField = {
|
||||
From: 'From',
|
||||
Recipient: 'Recipient',
|
||||
Subject: 'Subject',
|
||||
Header: 'Header',
|
||||
Body: 'Body',
|
||||
Size: 'Size'
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FilterConditionType = {
|
||||
Contains: 'Contains',
|
||||
NotContains: 'NotContains',
|
||||
EqualTo: 'EqualTo',
|
||||
NotEqualTo: 'NotEqualTo',
|
||||
Regex: 'Regex',
|
||||
Over: 'Over',
|
||||
Under: 'Under',
|
||||
Text: 'Text',
|
||||
Raw: 'Raw'
|
||||
};
|
||||
|
||||
export class FilterConditionModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addObservables({
|
||||
field: FilterConditionField.From,
|
||||
type: FilterConditionType.Contains,
|
||||
value: '',
|
||||
valueError: false,
|
||||
|
||||
valueSecond: '',
|
||||
valueSecondError: false
|
||||
});
|
||||
|
||||
this.template = koComputable(() => {
|
||||
const template = 'SettingsFiltersCondition';
|
||||
switch (this.field()) {
|
||||
case FilterConditionField.Body:
|
||||
return template + 'Body';
|
||||
case FilterConditionField.Size:
|
||||
return template + 'Size';
|
||||
case FilterConditionField.Header:
|
||||
return template + 'More';
|
||||
default:
|
||||
return template + 'Default';
|
||||
}
|
||||
});
|
||||
|
||||
this.addSubscribables({
|
||||
field: () => {
|
||||
this.value('');
|
||||
this.valueSecond('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
verify() {
|
||||
if (!this.value()) {
|
||||
this.valueError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FilterConditionField.Header === this.field() && !this.valueSecond()) {
|
||||
this.valueSecondError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// static reviveFromJson(json) {}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
// '@Object': 'Object/FilterCondition',
|
||||
Field: this.field(),
|
||||
Type: this.type(),
|
||||
Value: this.value(),
|
||||
ValueSecond: this.valueSecond()
|
||||
};
|
||||
}
|
||||
|
||||
cloneSelf() {
|
||||
const filterCond = new FilterConditionModel();
|
||||
|
||||
filterCond.field(this.field());
|
||||
filterCond.type(this.type());
|
||||
filterCond.value(this.value());
|
||||
filterCond.valueSecond(this.valueSecond());
|
||||
|
||||
return filterCond;
|
||||
}
|
||||
}
|
||||
333
dev/Sieve/Model/Script.js
Normal file
333
dev/Sieve/Model/Script.js
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
import { AbstractModel } from 'Sieve/Model/Abstract';
|
||||
import { FilterModel } from 'Sieve/Model/Filter';
|
||||
import { koArrayWithDestroy } from 'Sieve/Utils';
|
||||
|
||||
const SIEVE_FILE_NAME = 'rainloop.user';
|
||||
|
||||
// collectionToFileString
|
||||
function filtersToSieveScript(filters)
|
||||
{
|
||||
let eol = '\r\n',
|
||||
split = /.{0,74}/g,
|
||||
require = {},
|
||||
parts = [
|
||||
'# This is SnappyMail sieve script.',
|
||||
'# Please don\'t change anything here.',
|
||||
'# RAINLOOP:SIEVE',
|
||||
''
|
||||
];
|
||||
|
||||
const quote = string => '"' + string.trim().replace(/(\\|")/, '\\\\$1') + '"';
|
||||
const StripSpaces = string => string.replace(/\s+/, ' ').trim();
|
||||
|
||||
// conditionToSieveScript
|
||||
const conditionToString = (condition, require) =>
|
||||
{
|
||||
let result = '',
|
||||
type = condition.type(),
|
||||
field = condition.field(),
|
||||
value = condition.value().trim(),
|
||||
valueSecond = condition.valueSecond().trim();
|
||||
|
||||
if (value.length && ('Header' !== field || valueSecond.length)) {
|
||||
switch (type)
|
||||
{
|
||||
case 'NotEqualTo':
|
||||
result += 'not ';
|
||||
type = ':is';
|
||||
break;
|
||||
case 'EqualTo':
|
||||
type = ':is';
|
||||
break;
|
||||
case 'NotContains':
|
||||
result += 'not ';
|
||||
type = ':contains';
|
||||
break;
|
||||
case 'Text':
|
||||
case 'Raw':
|
||||
case 'Over':
|
||||
case 'Under':
|
||||
case 'Contains':
|
||||
type = ':' + type.toLowerCase();
|
||||
break;
|
||||
case 'Regex':
|
||||
type = ':regex';
|
||||
require.regex = 1;
|
||||
break;
|
||||
default:
|
||||
return '/* @Error: unknown type value ' + type + '*/ false';
|
||||
}
|
||||
|
||||
switch (field)
|
||||
{
|
||||
case 'From':
|
||||
result += 'header ' + type + ' ["From"]';
|
||||
break;
|
||||
case 'Recipient':
|
||||
result += 'header ' + type + ' ["To", "CC"]';
|
||||
break;
|
||||
case 'Subject':
|
||||
result += 'header ' + type + ' ["Subject"]';
|
||||
break;
|
||||
case 'Header':
|
||||
result += 'header ' + type + ' [' + quote(valueSecond) + ']';
|
||||
break;
|
||||
case 'Body':
|
||||
// :text :raw :content
|
||||
result += 'body ' + type + ' :contains';
|
||||
require.body = 1;
|
||||
break;
|
||||
case 'Size':
|
||||
result += 'size ' + type;
|
||||
break;
|
||||
default:
|
||||
return '/* @Error: unknown field value ' + field + ' */ false';
|
||||
}
|
||||
|
||||
if (('From' === field || 'Recipient' === field) && value.includes(',')) {
|
||||
result += ' [' + value.split(',').map(value => quote(value)).join(', ').trim() + ']';
|
||||
} else if ('Size' === field) {
|
||||
result += ' ' + value;
|
||||
} else {
|
||||
result += ' ' + quote(value);
|
||||
}
|
||||
|
||||
return StripSpaces(result);
|
||||
}
|
||||
|
||||
return '/* @Error: empty condition value */ false';
|
||||
};
|
||||
|
||||
// filterToSieveScript
|
||||
const filterToString = (filter, require) =>
|
||||
{
|
||||
let sTab = ' ',
|
||||
block = true,
|
||||
result = [],
|
||||
conditions = filter.conditions();
|
||||
|
||||
const errorAction = type => result.push(sTab + '# @Error (' + type + '): empty action value');
|
||||
|
||||
// Conditions
|
||||
if (1 < conditions.length) {
|
||||
result.push('Any' === filter.conditionsType()
|
||||
? 'if anyof('
|
||||
: 'if allof('
|
||||
);
|
||||
result.push(conditions.map(condition => sTab + conditionToString(condition, require)).join(',' + eol));
|
||||
result.push(')');
|
||||
} else if (conditions.length) {
|
||||
result.push('if ' + conditionToString(conditions[0], require));
|
||||
} else {
|
||||
block = false;
|
||||
}
|
||||
|
||||
// actions
|
||||
block ? result.push('{') : (sTab = '');
|
||||
|
||||
if (filter.actionMarkAsRead() && ['None','MoveTo','Forward'].includes(filter.actionType())) {
|
||||
require.imap4flags = 1;
|
||||
result.push(sTab + 'addflag "\\\\Seen";');
|
||||
}
|
||||
|
||||
let value = filter.actionValue().trim();
|
||||
value = value.length ? quote(value) : 0;
|
||||
switch (filter.actionType())
|
||||
{
|
||||
case 'None':
|
||||
break;
|
||||
case 'Discard':
|
||||
result.push(sTab + 'discard;');
|
||||
break;
|
||||
case 'Vacation':
|
||||
if (value) {
|
||||
require.vacation = 1;
|
||||
|
||||
let days = 1,
|
||||
subject = '',
|
||||
addresses = '',
|
||||
paramValue = filter.actionValueSecond().trim();
|
||||
|
||||
if (paramValue.length) {
|
||||
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
|
||||
}
|
||||
|
||||
paramValue = (filter.actionValueThird() || '').trim();
|
||||
if (paramValue.length) {
|
||||
days = Math.max(1, parseInt(paramValue, 10));
|
||||
}
|
||||
|
||||
paramValue = (filter.actionValueFourth() || '').trim()
|
||||
if (paramValue.length) {
|
||||
paramValue = paramValue.split(',').map(email =>
|
||||
email.trim().length ? quote(email) : ''
|
||||
).filter(email => email.length);
|
||||
if (paramValue.length) {
|
||||
addresses = ':addresses [' + paramValue.join(', ') + '] ';
|
||||
}
|
||||
}
|
||||
|
||||
result.push(sTab + 'vacation :days ' + days + ' ' + addresses + subject + value + ';');
|
||||
} else {
|
||||
errorAction('vacation');
|
||||
}
|
||||
break;
|
||||
case 'Reject': {
|
||||
if (value) {
|
||||
require.reject = 1;
|
||||
result.push(sTab + 'reject ' + value + ';');
|
||||
} else {
|
||||
errorAction('reject');
|
||||
}
|
||||
break; }
|
||||
case 'Forward':
|
||||
if (value) {
|
||||
if (filter.actionKeep()) {
|
||||
require.fileinto = 1;
|
||||
result.push(sTab + 'fileinto "INBOX";');
|
||||
}
|
||||
result.push(sTab + 'redirect ' + value + ';');
|
||||
} else {
|
||||
errorAction('redirect');
|
||||
}
|
||||
break;
|
||||
case 'MoveTo':
|
||||
if (value) {
|
||||
require.fileinto = 1;
|
||||
result.push(sTab + 'fileinto ' + value + ';');
|
||||
} else {
|
||||
errorAction('fileinto');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
filter.actionNoStop() || result.push(sTab + 'stop;');
|
||||
|
||||
block && result.push('}');
|
||||
|
||||
return result.join(eol);
|
||||
};
|
||||
|
||||
filters.forEach(filter => {
|
||||
parts.push([
|
||||
'/*',
|
||||
'BEGIN:FILTER:' + filter.id,
|
||||
'BEGIN:HEADER',
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(filter.toJson())))).match(split).join(eol) + 'END:HEADER',
|
||||
'*/',
|
||||
filter.enabled() ? '' : '/* @Filter is disabled ',
|
||||
filterToString(filter, require),
|
||||
filter.enabled() ? '' : '*/',
|
||||
'/* END:FILTER */',
|
||||
''
|
||||
].join(eol));
|
||||
});
|
||||
|
||||
require = Object.keys(require);
|
||||
return (require.length ? 'require ' + JSON.stringify(require) + ';' + eol : '') + eol + parts.join(eol);
|
||||
}
|
||||
|
||||
// fileStringToCollection
|
||||
function sieveScriptToFilters(script)
|
||||
{
|
||||
let regex = /BEGIN:HEADER([\s\S]+?)END:HEADER/gm,
|
||||
filters = [],
|
||||
json,
|
||||
filter;
|
||||
if (script.length && script.includes('RAINLOOP:SIEVE')) {
|
||||
while ((json = regex.exec(script))) {
|
||||
json = decodeURIComponent(escape(atob(json[1].replace(/\s+/g, ''))));
|
||||
if (json && json.length && (json = JSON.parse(json))) {
|
||||
json['@Object'] = 'Object/Filter';
|
||||
json.Conditions.forEach(condition => condition['@Object'] = 'Object/FilterCondition');
|
||||
filter = FilterModel.reviveFromJson(json);
|
||||
filter && filters.push(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
export class SieveScriptModel extends AbstractModel
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addObservables({
|
||||
name: '',
|
||||
active: false,
|
||||
body: '',
|
||||
|
||||
exists: false,
|
||||
nameError: false,
|
||||
askDelete: false,
|
||||
canBeDeleted: true,
|
||||
hasChanges: false
|
||||
});
|
||||
|
||||
this.filters = koArrayWithDestroy();
|
||||
// this.saving = ko.observable(false).extend({ debounce: 200 });
|
||||
|
||||
this.addSubscribables({
|
||||
name: () => this.hasChanges(true),
|
||||
filters: () => this.hasChanges(true),
|
||||
body: () => this.hasChanges(true)
|
||||
});
|
||||
}
|
||||
|
||||
filtersToRaw() {
|
||||
return filtersToSieveScript(this.filters);
|
||||
// this.body(filtersToSieveScript(this.filters));
|
||||
}
|
||||
|
||||
rawToFilters() {
|
||||
return sieveScriptToFilters(this.body());
|
||||
// this.filters(sieveScriptToFilters(this.body()));
|
||||
}
|
||||
|
||||
verify() {
|
||||
this.nameError(!this.name().trim());
|
||||
return !this.nameError();
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
name: this.name(),
|
||||
active: this.active() ? 1 : 0,
|
||||
body: this.body(),
|
||||
filters: this.filters.map(item => item.toJson())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Only 'rainloop.user' script supports filters
|
||||
*/
|
||||
allowFilters() {
|
||||
return SIEVE_FILE_NAME === this.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJsonScript} json
|
||||
* @returns {?SieveScriptModel}
|
||||
*/
|
||||
static reviveFromJson(json) {
|
||||
const script = super.reviveFromJson(json);
|
||||
if (script) {
|
||||
if (script.allowFilters()) {
|
||||
script.filters(
|
||||
Array.isArray(json.filters) && json.filters.length
|
||||
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
|
||||
: sieveScriptToFilters(script.body())
|
||||
);
|
||||
} else {
|
||||
script.filters([]);
|
||||
}
|
||||
script.canBeDeleted(SIEVE_FILE_NAME !== json.name);
|
||||
script.exists(true);
|
||||
script.hasChanges(false);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,89 @@
|
|||
import { SieveScriptModel } from 'Sieve/Model/Script';
|
||||
|
||||
export const
|
||||
// import { i18n } from 'Common/Translator';
|
||||
i18n = rl.i18n,
|
||||
|
||||
// import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
|
||||
|
||||
// import { koArrayWithDestroy } from 'External/ko';
|
||||
// With this we don't need delegateRunOnDestroy
|
||||
koArrayWithDestroy = data => {
|
||||
data = ko.observableArray(data);
|
||||
data.subscribe(changes =>
|
||||
changes.forEach(item =>
|
||||
'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
|
||||
)
|
||||
, data, 'arrayChange');
|
||||
return data;
|
||||
},
|
||||
|
||||
// import { koComputable } from 'External/ko';
|
||||
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||
|
||||
arrayToString = (arr, separator) =>
|
||||
arr.map(item => item.toString ? item.toString() : item).join(separator);
|
||||
arr.map(item => item.toString ? item.toString() : item).join(separator),
|
||||
/*
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
rl.i18n('NOTIFICATIONS/')
|
||||
},
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return getNotificationMessage(code)
|
||||
|| getNotificationMessage(parseInt(defCode, 10))
|
||||
|| '';
|
||||
},
|
||||
*/
|
||||
getNotification = code => 'ERROR ' + code,
|
||||
|
||||
Remote = rl.app.Remote,
|
||||
|
||||
// capabilities
|
||||
capa = ko.observableArray(),
|
||||
|
||||
// Sieve scripts SieveScriptModel
|
||||
scripts = koArrayWithDestroy(),
|
||||
|
||||
loading = ko.observable(false),
|
||||
serverError = ko.observable(false),
|
||||
serverErrorDesc = ko.observable(''),
|
||||
setError = text => {
|
||||
serverError(true);
|
||||
serverErrorDesc(text);
|
||||
},
|
||||
|
||||
updateList = () => {
|
||||
if (!loading()) {
|
||||
loading(true);
|
||||
serverError(false);
|
||||
|
||||
Remote.request('Filters', (iError, data) => {
|
||||
loading(false);
|
||||
scripts([]);
|
||||
|
||||
if (iError) {
|
||||
capa([]);
|
||||
setError(getNotification(iError));
|
||||
} else {
|
||||
capa(data.Result.Capa);
|
||||
/*
|
||||
scripts(
|
||||
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
|
||||
);
|
||||
*/
|
||||
forEachObjectValue(data.Result.Scripts, value => {
|
||||
value = SieveScriptModel.reviveFromJson(value);
|
||||
value && scripts.push(value)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
204
dev/Sieve/View/Filter.js
Normal file
204
dev/Sieve/View/Filter.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { FilterAction } from 'Sieve/Model/Filter';
|
||||
import { FilterConditionField, FilterConditionType } from 'Sieve/Model/FilterCondition';
|
||||
/*
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
*/
|
||||
|
||||
import {
|
||||
capa,
|
||||
i18n,
|
||||
koComputable
|
||||
} from 'Sieve/Utils';
|
||||
|
||||
const
|
||||
// import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||
defaultOptionsAfterRender = (domItem, item) =>
|
||||
domItem && item && undefined !== item.disabled
|
||||
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||
|
||||
// import { folderListOptionsBuilder } from 'Common/Folders';
|
||||
/**
|
||||
* @param {Array=} aDisabled
|
||||
* @param {Array=} aHeaderLines
|
||||
* @param {Function=} fRenameCallback
|
||||
* @returns {Array}
|
||||
*/
|
||||
folderListOptionsBuilder = (
|
||||
aDisabled,
|
||||
aHeaderLines,
|
||||
fRenameCallback
|
||||
) => {
|
||||
const
|
||||
aResult = [],
|
||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||
|
||||
foldersWalk = folders => {
|
||||
folders.forEach(oItem => {
|
||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||
aResult.push({
|
||||
id: oItem.fullName,
|
||||
name:
|
||||
sDeepPrefix.repeat(oItem.deep) +
|
||||
fRenameCallback(oItem),
|
||||
system: false,
|
||||
disabled: !oItem.selectable() || aDisabled.includes(oItem.fullName)
|
||||
});
|
||||
}
|
||||
|
||||
if (oItem.subFolders.length) {
|
||||
foldersWalk(oItem.subFolders());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
||||
Array.isArray(aDisabled) || (aDisabled = []);
|
||||
|
||||
Array.isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
||||
aResult.push({
|
||||
id: line[0],
|
||||
name: line[1],
|
||||
system: false,
|
||||
disabled: false
|
||||
})
|
||||
);
|
||||
|
||||
// FolderUserStore.folderList()
|
||||
foldersWalk(window.Sieve.folderList() || []);
|
||||
|
||||
return aResult;
|
||||
};
|
||||
|
||||
export class FilterPopupView extends rl.pluginPopupView {
|
||||
constructor() {
|
||||
super('Filter');
|
||||
|
||||
this.addObservables({
|
||||
isNew: true,
|
||||
filter: null,
|
||||
allowMarkAsRead: false,
|
||||
selectedFolderValue: ''
|
||||
});
|
||||
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
this.folderSelectList = koComputable(() =>
|
||||
folderListOptionsBuilder(
|
||||
[rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'],
|
||||
[['', '']],
|
||||
item => item ? item.localName() : ''
|
||||
)
|
||||
);
|
||||
|
||||
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
|
||||
|
||||
['actionTypeOptions','fieldOptions','typeOptions','typeOptionsSize','typeOptionsBody'].forEach(
|
||||
key => this[key] = ko.observableArray()
|
||||
);
|
||||
|
||||
this.populateOptions();
|
||||
}
|
||||
|
||||
saveFilter() {
|
||||
if (FilterAction.MoveTo === this.filter().actionType()) {
|
||||
this.filter().actionValue(this.selectedFolderValue());
|
||||
}
|
||||
|
||||
if (this.filter().verify()) {
|
||||
this.fTrueCallback();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
populateOptions() {
|
||||
this.actionTypeOptions([]);
|
||||
|
||||
let i18nFilter = key => i18n('POPUPS_FILTER/SELECT_' + key);
|
||||
|
||||
this.fieldOptions([
|
||||
{ id: FilterConditionField.From, name: i18n('GLOBAL/FROM') },
|
||||
{ id: FilterConditionField.Recipient, name: i18nFilter('FIELD_RECIPIENTS') },
|
||||
{ id: FilterConditionField.Subject, name: i18n('GLOBAL/SUBJECT') },
|
||||
{ id: FilterConditionField.Size, name: i18nFilter('FIELD_SIZE') },
|
||||
{ id: FilterConditionField.Header, name: i18nFilter('FIELD_HEADER') }
|
||||
]);
|
||||
|
||||
this.typeOptions([
|
||||
{ id: FilterConditionType.Contains, name: i18nFilter('TYPE_CONTAINS') },
|
||||
{ id: FilterConditionType.NotContains, name: i18nFilter('TYPE_NOT_CONTAINS') },
|
||||
{ id: FilterConditionType.EqualTo, name: i18nFilter('TYPE_EQUAL_TO') },
|
||||
{ id: FilterConditionType.NotEqualTo, name: i18nFilter('TYPE_NOT_EQUAL_TO') }
|
||||
]);
|
||||
|
||||
// this.actionTypeOptions.push({id: FilterAction.None,
|
||||
// name: i18n('GLOBAL/NONE')});
|
||||
if (capa) {
|
||||
this.allowMarkAsRead(capa.includes('imap4flags'));
|
||||
|
||||
if (capa.includes('fileinto')) {
|
||||
this.actionTypeOptions.push({
|
||||
id: FilterAction.MoveTo,
|
||||
name: i18nFilter('ACTION_MOVE_TO')
|
||||
});
|
||||
this.actionTypeOptions.push({
|
||||
id: FilterAction.Forward,
|
||||
name: i18nFilter('ACTION_FORWARD_TO')
|
||||
});
|
||||
}
|
||||
|
||||
if (capa.includes('reject')) {
|
||||
this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') });
|
||||
}
|
||||
|
||||
if (capa.includes('vacation')) {
|
||||
this.actionTypeOptions.push({
|
||||
id: FilterAction.Vacation,
|
||||
name: i18nFilter('ACTION_VACATION_MESSAGE')
|
||||
});
|
||||
}
|
||||
|
||||
if (capa.includes('body')) {
|
||||
this.fieldOptions.push({ id: FilterConditionField.Body, name: i18nFilter('FIELD_BODY') });
|
||||
}
|
||||
|
||||
if (capa.includes('regex')) {
|
||||
this.typeOptions.push({ id: FilterConditionType.Regex, name: 'Regex' });
|
||||
}
|
||||
}
|
||||
|
||||
this.actionTypeOptions.push({ id: FilterAction.Discard, name: i18nFilter('ACTION_DISCARD') });
|
||||
|
||||
this.typeOptionsSize([
|
||||
{ id: FilterConditionType.Over, name: i18nFilter('TYPE_OVER') },
|
||||
{ id: FilterConditionType.Under, name: i18nFilter('TYPE_UNDER') }
|
||||
]);
|
||||
|
||||
this.typeOptionsBody([
|
||||
{ id: FilterConditionType.Text, name: i18nFilter('TYPE_TEXT') },
|
||||
{ id: FilterConditionType.Raw, name: i18nFilter('TYPE_RAW') }
|
||||
]);
|
||||
}
|
||||
|
||||
removeCondition(oConditionToDelete) {
|
||||
this.filter().removeCondition(oConditionToDelete);
|
||||
}
|
||||
|
||||
beforeShow(oFilter, fTrueCallback, bEdit) {
|
||||
// onShow(oFilter, fTrueCallback, bEdit) {
|
||||
this.isNew(!bEdit);
|
||||
|
||||
this.fTrueCallback = fTrueCallback;
|
||||
this.filter(oFilter);
|
||||
|
||||
this.selectedFolderValue(oFilter.actionValue());
|
||||
|
||||
bEdit || oFilter.nameFocused(true);
|
||||
|
||||
this.populateOptions();
|
||||
}
|
||||
|
||||
afterShow() {
|
||||
this.isNew() && this.filter().nameFocused(true);
|
||||
}
|
||||
}
|
||||
136
dev/Sieve/View/Script.js
Normal file
136
dev/Sieve/View/Script.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { FilterModel } from 'Sieve/Model/Filter';
|
||||
import { SieveScriptModel } from 'Sieve/Model/Script';
|
||||
|
||||
import { FilterPopupView } from 'Sieve/View/Filter';
|
||||
|
||||
//import { parseScript } from 'Sieve/Parser';
|
||||
|
||||
import {
|
||||
capa,
|
||||
scripts,
|
||||
getNotification,
|
||||
Remote
|
||||
} from 'Sieve/Utils';
|
||||
|
||||
export class SieveScriptPopupView extends rl.pluginPopupView {
|
||||
constructor() {
|
||||
super('SieveScript');
|
||||
|
||||
this.addObservables({
|
||||
saveError: false,
|
||||
saveErrorText: '',
|
||||
rawActive: false,
|
||||
allowToggle: false,
|
||||
script: null
|
||||
});
|
||||
|
||||
this.sieveCapabilities = capa.join(' ');
|
||||
this.saving = false;
|
||||
|
||||
this.filterForDeletion = ko.observable(null).askDeleteHelper();
|
||||
}
|
||||
|
||||
saveScript() {
|
||||
let self = this,
|
||||
script = self.script();
|
||||
if (!self.saving/* && script.hasChanges()*/) {
|
||||
if (!script.verify()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!script.exists() && scripts.find(item => item.name() === script.name())) {
|
||||
script.nameError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
self.saving = true;
|
||||
self.saveError(false);
|
||||
|
||||
if (self.allowToggle()) {
|
||||
script.body(script.filtersToRaw());
|
||||
}
|
||||
|
||||
Remote.request('FiltersScriptSave',
|
||||
(iError, data) => {
|
||||
self.saving = false;
|
||||
|
||||
if (iError) {
|
||||
self.saveError(true);
|
||||
self.saveErrorText((data && data.ErrorMessageAdditional) || getNotification(iError));
|
||||
} else {
|
||||
script.exists() || scripts.push(script);
|
||||
script.exists(true);
|
||||
script.hasChanges(false);
|
||||
}
|
||||
},
|
||||
script.toJson()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deleteFilter(filter) {
|
||||
this.script().filters.remove(filter);
|
||||
}
|
||||
|
||||
addFilter() {
|
||||
/* this = SieveScriptModel */
|
||||
const filter = new FilterModel();
|
||||
filter.generateID();
|
||||
FilterPopupView.showModal([
|
||||
filter,
|
||||
() => this.filters.push(filter)
|
||||
]);
|
||||
}
|
||||
|
||||
editFilter(filter) {
|
||||
const clonedFilter = filter.cloneSelf();
|
||||
FilterPopupView.showModal([
|
||||
clonedFilter,
|
||||
() => {
|
||||
const script = this.script(),
|
||||
filters = script.filters(),
|
||||
index = filters.indexOf(filter);
|
||||
if (-1 < index) {
|
||||
// script.filters.splice(index, 1, clonedFilter);
|
||||
filters[index] = clonedFilter;
|
||||
script.filters(filters);
|
||||
}
|
||||
},
|
||||
true
|
||||
]);
|
||||
}
|
||||
|
||||
toggleFiltersRaw() {
|
||||
let script = this.script(), notRaw = !this.rawActive();
|
||||
if (notRaw) {
|
||||
script.body(script.filtersToRaw());
|
||||
script.hasChanges(script.hasChanges());
|
||||
}
|
||||
this.rawActive(notRaw);
|
||||
}
|
||||
|
||||
onBuild(oDom) {
|
||||
oDom.addEventListener('click', event => {
|
||||
const el = event.target.closestWithin('td.e-action', oDom),
|
||||
filter = el && ko.dataFor(el);
|
||||
filter && this.editFilter(filter);
|
||||
});
|
||||
}
|
||||
|
||||
beforeShow(oScript) {
|
||||
// onShow(oScript) {
|
||||
oScript = oScript || new SieveScriptModel();
|
||||
let raw = !oScript.allowFilters();
|
||||
this.script(oScript);
|
||||
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'));
|
||||
*/
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue