diff --git a/app/src/flux/mailsync-bridge.es6 b/app/src/flux/mailsync-bridge.es6 index ee909eea9..9c4bac79b 100644 --- a/app/src/flux/mailsync-bridge.es6 +++ b/app/src/flux/mailsync-bridge.es6 @@ -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') { diff --git a/app/src/flux/stores/mail-rules-store.es6 b/app/src/flux/stores/mail-rules-store.es6 index e28030066..c40a81e45 100644 --- a/app/src/flux/stores/mail-rules-store.es6 +++ b/app/src/flux/stores/mail-rules-store.es6 @@ -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(); diff --git a/app/src/flux/tasks/change-starred-task.es6 b/app/src/flux/tasks/change-starred-task.es6 index b28d3154d..f019f1d60 100644 --- a/app/src/flux/tasks/change-starred-task.es6 +++ b/app/src/flux/tasks/change-starred-task.es6 @@ -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'; } diff --git a/app/src/flux/tasks/change-unread-task.es6 b/app/src/flux/tasks/change-unread-task.es6 index 3f563b36b..085f7c9aa 100644 --- a/app/src/flux/tasks/change-unread-task.es6 +++ b/app/src/flux/tasks/change-unread-task.es6 @@ -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'; } diff --git a/app/src/mail-rules-processor.es6 b/app/src/mail-rules-processor.es6 index fd45fdb39..dc8abc5c4 100644 --- a/app/src/mail-rules-processor.es6 +++ b/app/src/mail-rules-processor.es6 @@ -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;