Run mail rules automatically as mail is received

This commit is contained in:
Ben Gotow 2017-10-19 14:23:02 -07:00
parent aad8c70498
commit 1950f5b356
5 changed files with 53 additions and 34 deletions

View file

@ -341,6 +341,7 @@ export default class MailsyncBridge {
DatabaseStore.trigger(record);
// Run task success / error handlers if the task is now complete
// Note: cannot use `record.objectClass` because of subclass names
if (record.type === 'persist' && record.objects[0] instanceof Task) {
for (const task of record.objects) {
if (task.status !== 'complete') {

View file

@ -11,13 +11,25 @@ import MailRulesProcessor from '../../mail-rules-processor';
import { ConditionMode, ConditionTemplates, ActionTemplates } from '../../mail-rules-templates';
const RulesJSONKey = 'MailRules-V2';
const AutoSinceJSONKey = 'MailRules-Auto-Since';
class MailRulesStore extends MailspringStore {
constructor() {
super();
/* This is a bit strange - if the user has mail rules enabled, they only
expect rules to be applied to "new" mail. Not "new" mail as in just created,
since that includes old mail we're syncing for the first time. Just "new"
mail that has arrived since they last ran Mailspring. So, we keep a date. */
this._autoSince = (window.localStorage.getItem(AutoSinceJSONKey) || 0) / 1;
if (this._autoSince === 0) {
window.localStorage.setItem(AutoSinceJSONKey, Date.now());
this._autoSince = Date.now();
}
this._reprocessing = {};
this._rules = [];
try {
const txt = window.localStorage.getItem(RulesJSONKey);
if (txt) {
@ -34,6 +46,8 @@ class MailRulesStore extends MailspringStore {
this.listenTo(Actions.disableMailRule, this._onDisableMailRule);
this.listenTo(Actions.startReprocessingMailRules, this._onStartReprocessing);
this.listenTo(Actions.stopReprocessingMailRules, this._onStopReprocessing);
this.listenTo(DatabaseStore, this._onDatabaseChanged);
}
rules() {
@ -52,6 +66,22 @@ class MailRulesStore extends MailspringStore {
return this._reprocessing;
}
_onDatabaseChanged = record => {
// If the record contains new emails, process mail rules immediately.
// This is necessary to avoid emails from bouncing through the inbox.
if (record.type === 'persist' && record.objectClass === Message.name) {
const newMessages = record.objects.filter(msg => {
if (msg.version !== 1) return false;
if (msg.draft) return false;
if (!msg.date || msg.date.valueOf() < this._autoSince) return false;
return true;
});
if (newMessages.length > 0) {
MailRulesProcessor.processMessages(newMessages);
}
}
};
_onDeleteMailRule = id => {
this._rules = this._rules.filter(f => f.id !== id);
this._saveMailRules();

View file

@ -13,16 +13,6 @@ export default class ChangeStarredTask extends ChangeMailTask {
}),
});
constructor(data = {}) {
if (data.threads) {
data.threads = data.threads.filter(t => t.starred !== data.starred);
}
if (data.messages) {
data.messages = data.messages.filter(m => m.starred !== data.starred);
}
super(data);
}
label() {
return this.starred ? 'Starring' : 'Unstarring';
}

View file

@ -13,16 +13,6 @@ export default class ChangeUnreadTask extends ChangeMailTask {
}),
});
constructor(data = {}) {
if (data.threads) {
data.threads = data.threads.filter(t => t.unread !== data.unread);
}
if (data.messages) {
data.messages = data.messages.filter(m => m.unread !== data.unread);
}
super(data);
}
label() {
return this.unread ? 'Marking as unread' : 'Marking as read';
}

View file

@ -24,10 +24,7 @@ information about the current view. Maybe after the unified inbox refactor...
*/
const MailRulesActions = {
markAsImportant: async (message, thread) => {
const important = await DatabaseStore.findBy(Label, {
role: 'important',
accountId: thread.accountId,
});
const important = CategoryStore.getCategoryByRole(thread.accountId, 'important');
if (!important) {
throw new Error('Could not find `important` label');
}
@ -52,6 +49,9 @@ const MailRulesActions = {
},
markAsRead: (message, thread) => {
if (thread.unread === false) {
return null;
}
return new ChangeUnreadTask({
unread: false,
threads: [thread],
@ -60,6 +60,9 @@ const MailRulesActions = {
},
star: (message, thread) => {
if (thread.starred === true) {
return null;
}
return new ChangeStarredTask({
starred: true,
threads: [thread],
@ -71,8 +74,8 @@ const MailRulesActions = {
if (!value) {
throw new Error('A folder is required.');
}
const folder = await DatabaseStore.findBy(Folder, { id: value, accountId: thread.accountId });
if (!folder) {
const folder = CategoryStore.byId(thread.accountId, value);
if (!folder || !(folder instanceof Folder)) {
throw new Error('The folder could not be found.');
}
return new ChangeFolderTask({
@ -86,8 +89,8 @@ const MailRulesActions = {
if (!value) {
throw new Error('A label is required.');
}
const label = await DatabaseStore.findBy(Label, { id: value, accountId: thread.accountId });
if (!label) {
const label = CategoryStore.byId(thread.accountId, value);
if (!label || !(label instanceof Label)) {
throw new Error('The label could not be found.');
}
return new ChangeLabelsTask({
@ -112,12 +115,11 @@ const MailRulesActions = {
throw new Error('A label is required.');
}
const { withId, withRole } = await Promise.props({
withId: DatabaseStore.findBy(Label, { id: roleOrId, accountId: thread.accountId }),
withRole: DatabaseStore.findBy(Label, { role: roleOrId, accountId: thread.accountId }),
});
const label = withId || withRole;
if (!label) {
const label = CategoryStore.categories(thread.accountId).find(
c => c.id === roleOrId || c.role === roleOrId
);
if (!label || !(label instanceof Label)) {
throw new Error('The label could not be found.');
}
return new ChangeLabelsTask({
@ -189,6 +191,12 @@ class MailRulesProcessor {
const actionResults = await Promise.all(actionPromises);
const actionTasks = actionResults.filter(r => r instanceof Task);
// mark that none of these tasks are undoable
actionTasks.forEach(t => {
t.canBeUndone = false;
});
const performLocalPromises = actionTasks.map(t => TaskQueue.waitForPerformLocal(t));
Actions.queueTasks(actionTasks);
await performLocalPromises;