diff --git a/spec/stores/draft-store-spec.es6 b/spec/stores/draft-store-spec.es6 index 6a3809efd..b82b749d2 100644 --- a/spec/stores/draft-store-spec.es6 +++ b/spec/stores/draft-store-spec.es6 @@ -412,7 +412,7 @@ xdescribe('DraftStore', function draftStore() { it("resets the sending state if there's an error", () => { spyOn(NylasEnv, "isMainWindow").andReturn(false); DraftStore._draftsSending[this.draft.clientId] = true; - Actions.sendDraftFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId}); + Actions.draftDeliveryFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId}); expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false); expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.clientId); }); @@ -423,7 +423,7 @@ xdescribe('DraftStore', function draftStore() { spyOn(remote.dialog, "showMessageBox"); spyOn(Actions, "composePopoutDraft"); DraftStore._draftsSending[this.draft.clientId] = true; - Actions.sendDraftFailed({threadId: 't1', errorMessage: "boohoo", draftClientId: this.draft.clientId}); + Actions.draftDeliveryFailed({threadId: 't1', errorMessage: "boohoo", draftClientId: this.draft.clientId}); advanceClock(400); expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false); expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.clientId); @@ -438,7 +438,7 @@ xdescribe('DraftStore', function draftStore() { spyOn(FocusedContentStore, "focused").andReturn({id: "t1"}); spyOn(Actions, "composePopoutDraft"); DraftStore._draftsSending[this.draft.clientId] = true; - Actions.sendDraftFailed({threadId: 't2', errorMessage: "boohoo", draftClientId: this.draft.clientId}); + Actions.draftDeliveryFailed({threadId: 't2', errorMessage: "boohoo", draftClientId: this.draft.clientId}); advanceClock(400); expect(Actions.composePopoutDraft).toHaveBeenCalled(); const call = Actions.composePopoutDraft.calls[0]; @@ -451,7 +451,7 @@ xdescribe('DraftStore', function draftStore() { spyOn(Actions, "composePopoutDraft"); DraftStore._draftsSending[this.draft.clientId] = true; spyOn(FocusedContentStore, "focused").andReturn(null); - Actions.sendDraftFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId}); + Actions.draftDeliveryFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId}); advanceClock(400); expect(Actions.composePopoutDraft).toHaveBeenCalled(); const call = Actions.composePopoutDraft.calls[0]; diff --git a/spec/stores/file-upload-store-spec.coffee b/spec/stores/file-upload-store-spec.coffee index c82ff698d..4bbb233c3 100644 --- a/spec/stores/file-upload-store-spec.coffee +++ b/spec/stores/file-upload-store-spec.coffee @@ -133,7 +133,7 @@ xdescribe 'FileUploadStore', -> describe "when a draft is sent", -> it "should delete its uploads directory", -> spyOn(FileUploadStore, '_deleteUploadsForClientId') - Actions.sendDraftSuccess({messageClientId: '123'}) + Actions.ensureMessageInSentSuccess({messageClientId: '123'}) expect(FileUploadStore._deleteUploadsForClientId).toHaveBeenCalledWith('123') describe '_getFileStats', -> diff --git a/spec/tasks/send-draft-task-spec.es6 b/spec/tasks/send-draft-task-spec.es6 index b88f3e066..fea2bc8a6 100644 --- a/spec/tasks/send-draft-task-spec.es6 +++ b/spec/tasks/send-draft-task-spec.es6 @@ -93,7 +93,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { spyOn(DBt, 'unpersistModel').andReturn(Promise.resolve()); spyOn(DBt, 'persistModel').andReturn(Promise.resolve()); spyOn(SoundRegistry, "playSound"); - spyOn(Actions, "sendDraftSuccess"); + spyOn(Actions, "draftDeliverySucceeded"); }); // The tests below are invoked twice, once with a new this.draft and one with a @@ -165,7 +165,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { it("should notify the draft was sent", () => { waitsForPromise(() => this.task.performRemote().then(() => { - const args = Actions.sendDraftSuccess.calls[0].args[0]; + const args = Actions.draftDeliverySucceeded.calls[0].args[0]; expect(args.message instanceof Message).toBe(true) expect(args.messageClientId).toBe(this.draft.clientId) })); @@ -203,7 +203,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { describe("when there are errors", () => { beforeEach(() => { - spyOn(Actions, 'sendDraftFailed'); + spyOn(Actions, 'draftDeliveryFailed'); jasmine.unspy(NylasAPIRequest.prototype, "run"); }); @@ -219,7 +219,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { waitsForPromise(() => this.task.performRemote().then((status) => { expect(status[0]).toBe(Task.Status.Failed); expect(status[1]).toBe(thrownError); - expect(Actions.sendDraftFailed).toHaveBeenCalled(); + expect(Actions.draftDeliveryFailed).toHaveBeenCalled(); expect(NylasEnv.reportError).toHaveBeenCalled(); })); }); @@ -284,7 +284,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { waitsForPromise(() => this.task.performRemote().then((status) => { expect(status[0]).toBe(Task.Status.Failed); expect(status[1]).toBe(thrownError); - expect(Actions.sendDraftFailed).toHaveBeenCalled(); + expect(Actions.draftDeliveryFailed).toHaveBeenCalled(); })); }); @@ -296,7 +296,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { waitsForPromise(() => this.task.performRemote().then((status) => { expect(status[0]).toBe(Task.Status.Failed); expect(status[1]).toBe(thrownError); - expect(Actions.sendDraftFailed).toHaveBeenCalled(); + expect(Actions.draftDeliveryFailed).toHaveBeenCalled(); })); }); @@ -325,9 +325,9 @@ xdescribe('SendDraftTask', function sendDraftTask() { waitsForPromise(() => this.task.performRemote().then((status) => { expect(status[0]).toBe(Task.Status.Failed); expect(status[1]).toBe(thrownError); - expect(Actions.sendDraftFailed).toHaveBeenCalled(); + expect(Actions.draftDeliveryFailed).toHaveBeenCalled(); - const msg = Actions.sendDraftFailed.calls[0].args[0].errorMessage; + const msg = Actions.draftDeliveryFailed.calls[0].args[0].errorMessage; expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage)); })); }); @@ -349,9 +349,9 @@ xdescribe('SendDraftTask', function sendDraftTask() { waitsForPromise(() => this.task.performRemote().then((status) => { expect(status[0]).toBe(Task.Status.Failed); expect(status[1]).toBe(thrownError); - expect(Actions.sendDraftFailed).toHaveBeenCalled(); + expect(Actions.draftDeliveryFailed).toHaveBeenCalled(); - const msg = Actions.sendDraftFailed.calls[0].args[0].errorMessage; + const msg = Actions.draftDeliveryFailed.calls[0].args[0].errorMessage; expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage)); })); }); @@ -509,7 +509,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { }); }); - describe("usingMultiSend", () => { + describe("hasCustomBodyPerRecipient", () => { beforeEach(() => { this.task = new SendDraftTask('client-id'); this.task.allowMultiSend = true; @@ -545,13 +545,13 @@ xdescribe('SendDraftTask', function sendDraftTask() { it("should return false if the provider is eas", () => { this.applySpies({"AccountStore.accountForId": "eas"}) - expect(this.task.usingMultiSend()).toBe(false); + expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return false if allowMultiSend is false", () => { this.applySpies(); this.task.allowMultiSend = false; - expect(this.task.usingMultiSend()).toBe(false); + expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return false if the open-tracking id is null", () => { @@ -559,7 +559,7 @@ xdescribe('SendDraftTask', function sendDraftTask() { return name === "open-tracking" ? null : name; }; this.applySpies({"NylasEnv.packages.pluginIdFor": fake}); - expect(this.task.usingMultiSend()).toBe(false); + expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return false if the link-tracking id is null", () => { @@ -567,36 +567,36 @@ xdescribe('SendDraftTask', function sendDraftTask() { return name === "link-tracking" ? null : name; }; this.applySpies({"NylasEnv.packages.pluginIdFor": fake}); - expect(this.task.usingMultiSend()).toBe(false); + expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return false if neither open-tracking nor link-tracking is on", () => { this.applySpies(); this.task.draft.applyPluginMetadata('open-tracking', false); this.task.draft.applyPluginMetadata('link-tracking', false); - expect(this.task.usingMultiSend()).toBe(false); + expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return true if only open-tracking is on", () => { this.applySpies(); this.task.draft.applyPluginMetadata('link-tracking', false); - expect(this.task.usingMultiSend()).toBe(true); + expect(this.task.hasCustomBodyPerRecipient()).toBe(true); }); it("should return true if only link-tracking is on", () => { this.applySpies(); this.task.draft.applyPluginMetadata('open-tracking', false); - expect(this.task.usingMultiSend()).toBe(true); + expect(this.task.hasCustomBodyPerRecipient()).toBe(true); }); it("should return false if there are too many participants", () => { this.applySpies({"draft.participants": 15}); - expect(this.task.usingMultiSend()).toBe(false); + expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return true otherwise", () => { this.applySpies(); - expect(this.task.usingMultiSend()).toBe(true); + expect(this.task.hasCustomBodyPerRecipient()).toBe(true); }); }); }); diff --git a/src/K2 b/src/K2 index ab9eeaafc..09f19d1ad 160000 --- a/src/K2 +++ b/src/K2 @@ -1 +1 @@ -Subproject commit ab9eeaafce307976e2639cfc089ff534a7ba2082 +Subproject commit 09f19d1adfec03c99fae0b4a1610b6c51e47f491 diff --git a/src/flux/actions.es6 b/src/flux/actions.es6 index af8f867c7..2d12476f1 100644 --- a/src/flux/actions.es6 +++ b/src/flux/actions.es6 @@ -423,8 +423,11 @@ class Actions { Recieves the clientId of the message that was sent */ - static sendDraftSuccess = ActionScopeGlobal; - static sendDraftFailed = ActionScopeGlobal; + static draftDeliverySucceeded = ActionScopeGlobal; + static draftDeliveryFailed = ActionScopeGlobal; + + static ensureMessageInSentSuccess = ActionScopeGlobal; + static ensureMessageInSentFailed = ActionScopeGlobal; static sendManyDrafts = ActionScopeWindow; static ensureDraftSynced = ActionScopeWindow; diff --git a/src/flux/stores/draft-store.es6 b/src/flux/stores/draft-store.es6 index 7f73a39cd..cf9c9869a 100644 --- a/src/flux/stores/draft-store.es6 +++ b/src/flux/stores/draft-store.es6 @@ -41,8 +41,8 @@ class DraftStore extends NylasStore { this.listenTo(Actions.composePopoutDraft, this._onPopoutDraftClientId); this.listenTo(Actions.composeNewBlankDraft, this._onPopoutBlankDraft); this.listenTo(Actions.composeNewDraftToRecipient, this._onPopoutNewDraftToRecipient); - this.listenTo(Actions.sendDraftFailed, this._onSendDraftFailed); - this.listenTo(Actions.sendDraftSuccess, this._onSendDraftSuccess); + this.listenTo(Actions.draftDeliveryFailed, this._onSendDraftFailed); + this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess); this.listenTo(Actions.didCancelSendAction, this._onDidCancelSendAction); this.listenTo(Actions.sendQuickReply, this._onSendQuickReply); diff --git a/src/flux/stores/file-upload-store.es6 b/src/flux/stores/file-upload-store.es6 index d4e075275..3a65add4f 100644 --- a/src/flux/stores/file-upload-store.es6 +++ b/src/flux/stores/file-upload-store.es6 @@ -49,7 +49,7 @@ class FileUploadStore extends NylasStore { mkdirp.sync(UPLOAD_DIR); if (NylasEnv.isMainWindow() || NylasEnv.inSpecMode()) { - this.listenTo(Actions.sendDraftSuccess, ({messageClientId}) => { + this.listenTo(Actions.ensureMessageInSentSuccess, ({messageClientId}) => { this._deleteUploadsForClientId(messageClientId); }); } diff --git a/src/flux/syncback-task-api-request.es6 b/src/flux/syncback-task-api-request.es6 index 8a9165eee..70faf5468 100644 --- a/src/flux/syncback-task-api-request.es6 +++ b/src/flux/syncback-task-api-request.es6 @@ -41,7 +41,7 @@ class SyncbackTaskAPIRequest { message: failed.error.message, data: failed.error.data, }, - statusCode: failed.error.statusCode, + statusCode: failed.error.statusCode || 500, }) reject(error) } else if (succeeded) { diff --git a/src/flux/tasks/reconcile-multi-send-task.es6 b/src/flux/tasks/ensure-message-in-sent-folder-task.es6 similarity index 64% rename from src/flux/tasks/reconcile-multi-send-task.es6 rename to src/flux/tasks/ensure-message-in-sent-folder-task.es6 index 8f845f4e9..6e20a7d73 100644 --- a/src/flux/tasks/reconcile-multi-send-task.es6 +++ b/src/flux/tasks/ensure-message-in-sent-folder-task.es6 @@ -1,14 +1,16 @@ import Task from './task'; import {APIError} from '../errors'; +import Actions from '../actions'; import NylasAPI from '../nylas-api'; import SyncbackTaskAPIRequest from '../syncback-task-api-request'; import SendDraftTask from './send-draft-task'; -export default class ReconcileMultiSendTask extends Task { +export default class EnsureMessageInSentFolderTask extends Task { constructor(opts = {}) { super(opts); this.message = opts.message; + this.sentPerRecipient = opts.sentPerRecipient; } isDependentOnTask(other) { @@ -27,18 +29,24 @@ export default class ReconcileMultiSendTask extends Task { return new SyncbackTaskAPIRequest({ api: NylasAPI, options: { - timeout: 1000 * 60 * 5, // We cannot hang up a send - won't know if it sent - method: "DELETE", - path: `/send-multiple/${this.message.id}`, + path: `/ensure-message-in-sent-folder/${this.message.id}`, + method: "POST", + body: { + sentPerRecipient: this.sentPerRecipient, + }, accountId: this.message.accountId, }, }) .run() - .thenReturn(Task.Status.Success) + .then(() => { + Actions.ensureMessageInSentSuccess({messageClientId: this.message.clientId}) + return Task.Status.Success + }) .catch((err) => { - const errorMessage = `We had trouble saving your message, "${this.message.subject}", to your Sent folder.\n\n${err.message}`; + const errorMessage = `Your message successfully sent; however, we had trouble saving your message, "${this.message.subject}", to your Sent folder.\n\n${err.message}`; + Actions.ensureMessageInSentFailed() if (err instanceof APIError) { - if (SyncbackTaskAPIRequest.PermanentErrorCodes.includes(err.statusCode)) { + if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) { NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true, detail: err.stack}); return Promise.resolve([Task.Status.Failed, err]); } diff --git a/src/flux/tasks/send-draft-task.es6 b/src/flux/tasks/send-draft-task.es6 index 88660bb6f..09fb51597 100644 --- a/src/flux/tasks/send-draft-task.es6 +++ b/src/flux/tasks/send-draft-task.es6 @@ -11,8 +11,7 @@ import DatabaseStore from '../stores/database-store'; import AccountStore from '../stores/account-store'; import BaseDraftTask from './base-draft-task'; import SyncbackMetadataTask from './syncback-metadata-task'; -import ReconcileMultiSendTask from './reconcile-multi-send-task'; - +import EnsureMessageInSentFolderTask from './ensure-message-in-sent-folder-task'; const OPEN_TRACKING_ID = NylasEnv.packages.pluginIdFor('open-tracking') const LINK_TRACKING_ID = NylasEnv.packages.pluginIdFor('link-tracking') @@ -38,6 +37,7 @@ export default class SendDraftTask extends BaseDraftTask { return this.refreshDraftReference() .then(this.assertDraftValidity) .then(this.sendMessage) + .then(this.ensureInSentFolder) .then(this.updatePluginMetadata) .then(this.onSuccess) .catch(this.onError); @@ -58,7 +58,7 @@ export default class SendDraftTask extends BaseDraftTask { return Promise.resolve(); } - usingMultiSend = () => { + hasCustomBodyPerRecipient = () => { if (!this.allowMultiSend) { return false; } @@ -73,17 +73,29 @@ export default class SendDraftTask extends BaseDraftTask { if (!pluginsAvailable) { return false; } - const pluginsInUse = (this.draft.metadataForPluginId(OPEN_TRACKING_ID) || this.draft.metadataForPluginId(LINK_TRACKING_ID)); + const pluginsInUse = (this.draft.metadataForPluginId(OPEN_TRACKING_ID) || this.draft.metadataForPluginId(LINK_TRACKING_ID)) || false; const providerCompatible = (AccountStore.accountForId(this.draft.accountId).provider !== "eas"); return pluginsInUse && providerCompatible; } - sendMessage = () => { - return this.usingMultiSend() ? this.sendWithMultipleBodies() : this.sendWithSingleBody(); + sendMessage = async () => { + if (this.hasCustomBodyPerRecipient()) { + await this._sendPerRecipient(); + } else { + await this._sendWithSingleBody() + } } - sendWithSingleBody = () => { - return new SyncbackTaskAPIRequest({ + ensureInSentFolder = () => { + const t = new EnsureMessageInSentFolderTask({ + message: this.message, + sentPerRecipient: this.hasCustomBodyPerRecipient(), + }) + Actions.queueTask(t) + } + + _sendWithSingleBody = async () => { + const task = new SyncbackTaskAPIRequest({ api: NylasAPI, options: { path: "/send", @@ -95,17 +107,15 @@ export default class SendDraftTask extends BaseDraftTask { requestId: this.draft.clientId, }, }) - .run() - .then((responseJSON) => { - return this.createMessageFromResponse(responseJSON) - }) + const responseJSON = await task.run(); + await this._createMessageFromResponse(responseJSON) } - sendWithMultipleBodies = () => { - return new SyncbackTaskAPIRequest({ + _sendPerRecipient = async () => { + const task = new SyncbackTaskAPIRequest({ api: NylasAPI, options: { - path: "/send-multiple", + path: "/send-per-recipient", accountId: this.draft.accountId, method: 'POST', body: { @@ -116,16 +126,8 @@ export default class SendDraftTask extends BaseDraftTask { timeout: 1000 * 60 * 5, // We cannot hang up a send - won't know if it sent }, }) - .run() - .then((responseJSON) => { - return this.createMessageFromResponse(responseJSON); - }) - .then(() => { - const t = new ReconcileMultiSendTask({ - message: this.message, - }); - Actions.queueTask(t); - }) + const responseJSON = await task.run(); + await this._createMessageFromResponse(responseJSON); } updatePluginMetadata = () => { @@ -138,7 +140,7 @@ export default class SendDraftTask extends BaseDraftTask { return Promise.resolve(); } - createMessageFromResponse = (responseJSON) => { + _createMessageFromResponse = (responseJSON) => { const {failedRecipients, message} = responseJSON if (failedRecipients && failedRecipients.length > 0) { const errorMessage = `We had trouble sending this message to all recipients. ${failedRecipients} may not have received this email.`; @@ -160,7 +162,7 @@ export default class SendDraftTask extends BaseDraftTask { onSuccess = () => { Actions.recordUserEvent("Draft Sent") - Actions.sendDraftSuccess({message: this.message, messageClientId: this.message.clientId, draftClientId: this.draft.clientId}); + Actions.draftDeliverySucceeded({message: this.message, messageClientId: this.message.clientId, draftClientId: this.draft.clientId}); // TODO we shouldn't need to do this anymore NylasAPIHelpers.makeDraftDeletionRequest(this.draft); @@ -197,7 +199,7 @@ export default class SendDraftTask extends BaseDraftTask { } if (this.emitError && !(err instanceof RequestEnsureOnceError)) { - Actions.sendDraftFailed({ + Actions.draftDeliveryFailed({ threadId: this.draft.threadId, draftClientId: this.draft.clientId, errorMessage: message, diff --git a/src/global/nylas-exports.es6 b/src/global/nylas-exports.es6 index a4f6798da..81a518e79 100644 --- a/src/global/nylas-exports.es6 +++ b/src/global/nylas-exports.es6 @@ -125,7 +125,7 @@ lazyLoadAndRegisterTask(`SyncbackCategoryTask`, 'syncback-category-task'); lazyLoadAndRegisterTask(`SyncbackMetadataTask`, 'syncback-metadata-task'); lazyLoadAndRegisterTask(`PerformSendActionTask`, 'perform-send-action-task'); lazyLoadAndRegisterTask(`ReprocessMailRulesTask`, 'reprocess-mail-rules-task'); -lazyLoadAndRegisterTask(`ReconcileMultiSendTask`, 'reconcile-multi-send-task'); +lazyLoadAndRegisterTask(`EnsureMessageInSentFolderTask`, 'ensure-message-in-sent-folder-task'); // Stores // These need to be required immediately since some Stores are