Skip to content

Commit

Permalink
Merge pull request #79 from christophehenry/WIP-InboxFilter
Browse files Browse the repository at this point in the history
Add setFilter and FilterRule
  • Loading branch information
fabienmoyon committed Sep 7, 2018
2 parents 81b308b + 6e3320c commit 5258070
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 27 deletions.
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"beforeEach": false,
"browser": false,
"describe": false,
"context": false,
"inject": false,
"element": false,
"it": false,
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [master]

## [0.0.29]
### Added
- add JMap's filter feature #79

## [0.0.28] - 2018-05-17
### Added
- add 'isForwarded' property to 'Message' #77
Expand Down
47 changes: 24 additions & 23 deletions lib/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,36 @@
* @module API
*/
export default {
Client: require('./client/Client'),
Utils: require('./utils/Utils'),
JSONBuilder: require('./utils/JSONBuilder'),
PromiseProvider: require('./promises/PromiseProvider'),
ES6PromiseProvider: require('./promises/ES6PromiseProvider'),
QPromiseProvider: require('./promises/QPromiseProvider'),
Transport: require('./transport/Transport'),
JQueryTransport: require('./transport/JQueryTransport'),
RequestTransport: require('./transport/RequestTransport'),
Model: require('./models/Model'),
Account: require('./models/Account'),
EMailer: require('./models/EMailer'),
Mailbox: require('./models/Mailbox'),
MessageList: require('./models/MessageList'),
Message: require('./models/Message'),
OutboundMessage: require('./models/OutboundMessage'),
CreateMessageAck: require('./models/CreateMessageAck'),
Thread: require('./models/Thread'),
MailboxRole: require('./models/MailboxRole'),
SetResponse: require('./models/SetResponse'),
Attachment: require('./models/Attachment'),
AuthAccess: require('./models/AuthAccess'),
AuthContinuation: require('./models/AuthContinuation'),
AuthMethod: require('./models/AuthMethod'),
Constants: require('./utils/Constants'),
Attachment: require('./models/Attachment'),
Capabilities: require('./models/Capabilities'),
Client: require('./client/Client'),
CreateMessageAck: require('./models/CreateMessageAck'),
Constants: require('./utils/Constants'),
EMailer: require('./models/EMailer'),
ES6PromiseProvider: require('./promises/ES6PromiseProvider'),
FilterRule: require('./models/FilterRule'),
JmapError: require('./errors/JmapError'),
JQueryTransport: require('./transport/JQueryTransport'),
JSONBuilder: require('./utils/JSONBuilder'),
Mailbox: require('./models/Mailbox'),
MailboxRole: require('./models/MailboxRole'),
MailCapabilities: require('./models/MailCapabilities'),
Message: require('./models/Message'),
MessageList: require('./models/MessageList'),
Model: require('./models/Model'),
OutboundMessage: require('./models/OutboundMessage'),
PromiseProvider: require('./promises/PromiseProvider'),
QPromiseProvider: require('./promises/QPromiseProvider'),
RequestTransport: require('./transport/RequestTransport'),
ServerCapabilities: require('./models/ServerCapabilities'),
VacationResponse: require('./models/VacationResponse'),
SetResponse: require('./models/SetResponse'),
Thread: require('./models/Thread'),
Transport: require('./transport/Transport'),
TransportError: require('./errors/TransportError'),
JmapError: require('./errors/JmapError')
Utils: require('./utils/Utils'),
VacationResponse: require('./models/VacationResponse'),
};
29 changes: 29 additions & 0 deletions lib/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import AuthAccess from './../models/AuthAccess.js';
import Constants from '../utils/Constants.js';
import VacationResponse from './../models/VacationResponse';
import JmapError from '../errors/JmapError';
import FilterRule from '../models/FilterRule';

export default class Client {
/**
Expand Down Expand Up @@ -664,6 +665,34 @@ export default class Client {
return Date.now();
}

/**
* Sets the singleton {@link FilterRule} instance for a given account.<br />
* This will send a `setFilter` request to the JMAP backend.
*
* @param filterRules {FilterRule[]|Object[]} The list of filter rules to set
* @param [options] {Object} The options to the underlying `setFilter` JMAP request.
* @param [options.accountId=null] {String} The account ID to set the filtering rules for. If `null`, the primary account is used.
*
* @returns {Promise} A {@link Promise} that eventually resolves to nothing upon success.
*/
setFilter(filterRules, options) {
const filterRulesAsJson = filterRules.map(rule => rule instanceof FilterRule ? rule.toJSONObject() : rule);

return this._jmapRequest('setFilter', {
accountId: options && options.accountId,
ifInState: options && options.ifInState || null,
[FilterRule.ID]: filterRulesAsJson
}).then(response => {
if (response.updated.indexOf(FilterRule.ID) < 0) {
throw new Error('Failed to set filter. Error: ' + (response.notUpdated[FilterRule.ID] || 'none'));
}
});
}

getFilter(options) {
return this._jmapRequest('getFilter', options).then(response => response.singleton);
}

_defaultNonAuthenticatedHeaders() {
return {
Accept: 'application/json; charset=UTF-8',
Expand Down
87 changes: 87 additions & 0 deletions lib/models/FilterRule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

import Model from './Model.js';
import uuid from 'uuid/v4';
import filterRuleConditions from './filterRules/FilterRuleConditions';
import filterRuleActions from './filterRules/FilterRuleActions';
import Utils from '../utils/Utils';

class FilterRule extends Model {
/**
* This class represents a JMAP [FilterRule]{@link http://jmap.io/spec.html}.<br />
* The _FilterRule_ object represents the state of incoming message filtering for an account.
*
* @constructor
* @extends Model
*
* @param jmap {Client} The {@link Client} instance that created this _FilterRule_.
* @param name {String} The name of the rule
*
* NOTE: How to use and extend this model ?
* FilterRule is conceived so that a rule can be specified in a highly didactic way. For instance:
*
* new jmap.FilterRule(client, 'My filter')
* .when.from
* .value('admin@open-paas.org')
* .comparator(jmap.FilterRule.Comparator.EXACTLY_EQUALS)
* .then.moveTo
* .mailboxId('36e4d1c0-a473-11e8-aa26-bfb5d32a28f6');
*
* To achive this, it uses the builder design pattern. To extend this model with new actions and conditions,
* you just need to create a new class that extends AbstractConditionAction and implements
* AbstractConditionAction#_init and AbstractConditionAction#_toJSONObject.
*
* AbstractConditionAction#_init is called by AbstractConditionAction' constructor and
* AbstractConditionAction#_toJSONObject is used to generate a JSON representation of the object.
* Then, just provide any useful property.
*
* To make the new condition or action available to the builder, you need to extend FilterRuleCondition
* (if defining a new condition) or FilterRuleAction (if defining a new action)
*
* @see Model
*/
constructor(jmap, name) {
super(jmap);

this.id = uuid();
this.name = name;
this.filterCondition = null;
this.filterAction = null;
}

get then() {
return filterRuleActions(this);
}

get when() {
return filterRuleConditions(this);
}

toJSONObject() {
Utils.assertRequiredParameterIsPresent(this.filterCondition, '', `Filter must have a condition. Use 'when'.`);
Utils.assertRequiredParameterIsPresent(this.filterAction, '', `Filter must have an action. Use 'then'.`);
this.filterCondition._validate();
this.filterAction._validate();

return {
id: this.id,
name: this.name,
condition: this.filterCondition._toJSONObject(),
action: this.filterAction._toJSONObject()
};
}

static fromJSONObject(jmap, object) {
throw new Error('Not implemented');
}
}

FilterRule.ID = 'singleton';
FilterRule.Comparator = Object.freeze({
CONTAINS: 'contains',
NOT_CONTAINS: 'not-contains',
EXACTLY_EQUALS: 'exactly-equals',
NOT_EXACTLY_EQUALS: 'not-exactly-equals'
});

export default FilterRule;
55 changes: 55 additions & 0 deletions lib/models/filterRules/AbstractConditionAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export default class AbstractConditionAction {
constructor(filterRule) {
this.filterRule = filterRule;
this._init();
}

/**
* Do not override
* @returns {*}
*/
get when() {
return this.filterRule.when;
}

/**
* Do not override
* @returns {*}
*/
get then() {
return this.filterRule.then;
}

/**
* Do not override
* @returns {*|JSON|{id, name, condition, action}}
*/
toJSONObject() {
return this.filterRule.toJSONObject();
}

/**
* Initialises the object. Called by the constructor
* @private
*/
_init() {
throw new Error('_init not implemented in child class');
}

/**
* Creates a JSON representation of the model
* @private
*/
_toJSONObject() {
throw new Error('_toJSONObject not implemented in child class');
}

/**
* Validates the object is correct with respect to JMap specification
* Will be called by FilterRule#toJSONObject
* @private
*/
_validate() {
throw new Error('_validate not implemented in child class');
}
}
58 changes: 58 additions & 0 deletions lib/models/filterRules/FilterRuleActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

import AbstractConditionAction from './AbstractConditionAction';
import Utils from '../../utils/Utils';

class MoveTo extends AbstractConditionAction {
_init() {
this._mailboxId = null;
}

mailboxId(val) {
this._mailboxId = val;

return this;
}

_toJSONObject() {
return {
appendIn: {
mailboxIds: [this._mailboxId]
}
};
}

_validate() {
Utils.assertRequiredParameterIsPresent(this._mailboxId, '', `Mailbox id is not set. Use mailboxId()`);
}
}

export default function filterRuleActions(filterRule) {
/**
* Intermediate object to inject a action the to filter
*
* How to extend:
* Create a new condition class extending {AbstractConditionAction} and make it available
* by defining a new getter in the returned object. For instance:
*
* get moveTo() {
* filterRule.filterAction = new MoveTo(filterRule);
* return filterRule.filterAction;
* }
*
* get delete() {
* filterRule.filterCondition = new Delete(filterRule);
* return filterRule.filterCondition
* }
*
* @param filterRule {FilterRule} The filter that is being constructed
* @returns {AbstractConditionAction} The new condition
*/
return {
get moveTo() {
filterRule.filterAction = new MoveTo(filterRule);

return filterRule.filterAction;
}
};
}
70 changes: 70 additions & 0 deletions lib/models/filterRules/FilterRuleConditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict';

import AbstractConditionAction from './AbstractConditionAction';
import FilterRule from '../FilterRule';
import Utils from '../../utils/Utils';

class From extends AbstractConditionAction {
_init() {
this._comparator = null;
this._value = null;
}

value(val) {
this._value = val;

return this;
}

comparator(val) {
if (!Utils.objectValuesIncludes(FilterRule.Comparator, val)) {
throw new Error(`From#comparator(): ${val} must be one of ${Utils.objectValues(FilterRule.Comparator)}`);
}
this._comparator = val;

return this;
}

_toJSONObject() {
return {
field: 'from',
comparator: this._comparator,
value: this._value,
};
}

_validate() {
Utils.assertRequiredParameterIsPresent(this._comparator, '', 'Comprator is not set. Use comparator()');
Utils.assertRequiredParameterIsPresent(this._value, '', 'Value is not set. Use value()');
}
}

export default function filterRuleConditions(filterRule) {
/**
* Intermediate object to inject a condition the to filter
*
* How to extend:
* Create a new condition class extending {AbstractConditionAction} and make it available
* by defining a new getter in the returned obect. For instance:
*
* get from() {
* filterRule.filterCondition = new From(filterRule);
* return filterRule.filterCondition;
* }
*
* get subject() {
* filterRule.filterCondition = new Subject(filterRule);
* return filterRule.filterCondition
* }
*
* @param filterRule {FilterRule} The filter that is being constructed
* @returns {AbstractConditionAction} The new condition
*/
return {
get from() {
filterRule.filterCondition = new From(filterRule);

return filterRule.filterCondition;
}
};
}
Loading

0 comments on commit 5258070

Please sign in to comment.