Send drafts through the new mailsync process

This commit is contained in:
Ben Gotow 2017-07-05 10:34:10 -07:00
parent 0377bcfb33
commit da2dbe43c9
14 changed files with 44 additions and 273 deletions

View file

@ -559,7 +559,8 @@ export default class ComposerView extends React.Component {
}
_onDestroyDraft = () => {
Actions.destroyDraft(this.props.draft.headerMessageId);
const {draft} = this.props;
Actions.destroyDraft(draft.accountId, draft.headerMessageId);
}
_onSelectAttachment = () => {

View file

@ -47,6 +47,6 @@ class DraftList extends React.Component
_onRemoveFromView: =>
drafts = DraftListStore.dataSource().selection.items()
Actions.destroyDraft(draft.headerMessageId) for draft in drafts
Actions.destroyDraft(draft.accountId, draft.headerMessageId) for draft in drafts
module.exports = DraftList

View file

@ -19,7 +19,7 @@ class DraftDeleteButton extends React.Component
_destroySelected: =>
for item in @props.selection.items()
Actions.destroyDraft(item.headerMessageId)
Actions.destroyDraft(item.accountId, item.headerMessageId)
@props.selection.clear()
return

View file

@ -39,10 +39,11 @@ class AccountIMAPSettingsForm extends React.Component {
errorMessage = "Please provide a valid hostname or IP adddress.";
errorFieldNames.push(`${type}_host`);
}
if (accountInfo[`${type}_host`] === 'imap.gmail.com') {
errorMessage = "Please link Gmail accounts by choosing 'Google' on the account type screen.";
errorFieldNames.push(`${type}_host`);
}
// todo bg
// if (accountInfo[`${type}_host`] === 'imap.gmail.com') {
// errorMessage = "Please link Gmail accounts by choosing 'Google' on the account type screen.";
// errorFieldNames.push(`${type}_host`);
// }
if (!Number.isInteger(accountInfo[`${type}_port`] / 1)) {
errorMessage = "Please provide a valid port number.";
errorFieldNames.push(`${type}_port`);

View file

@ -17,7 +17,7 @@ export function sendActions() {
return draft.threadId != null
},
performSendAction({draft}) {
Actions.queueTask(new SendDraftTask(draft.id))
Actions.queueTask(new SendDraftTask(draft))
return DatabaseStore.modelify(Thread, [draft.threadId])
.then((threads) => {
const tasks = TaskFactory.tasksForArchiving({

View file

@ -1,117 +0,0 @@
import {
Message,
DatabaseStore,
} from 'nylas-exports';
import BaseDraftTask from '../../src/flux/tasks/base-draft-task';
xdescribe('BaseDraftTask', function baseDraftTask() {
describe("shouldDequeueOtherTask", () => {
it("should dequeue instances of the same subclass for the same draft which are older", () => {
class ATask extends BaseDraftTask {
}
class BTask extends BaseDraftTask {
}
const A = new ATask('localid-A');
A.sequentialId = 1;
const B1 = new BTask('localid-A');
B1.sequentialId = 2;
const B2 = new BTask('localid-A');
B2.sequentialId = 3;
const BOther = new BTask('localid-other');
BOther.sequentialId = 4;
expect(B1.shouldDequeueOtherTask(A)).toBe(false);
expect(A.shouldDequeueOtherTask(B1)).toBe(false);
expect(B2.shouldDequeueOtherTask(B1)).toBe(true);
expect(B1.shouldDequeueOtherTask(B2)).toBe(false);
expect(BOther.shouldDequeueOtherTask(B2)).toBe(false);
expect(B2.shouldDequeueOtherTask(BOther)).toBe(false);
});
});
describe("isDependentOnTask", () => {
it("should always wait on older tasks for the same draft", () => {
const A = new BaseDraftTask('localid-A');
A.sequentialId = 1;
const B = new BaseDraftTask('localid-A');
B.sequentialId = 2;
expect(B.isDependentOnTask(A)).toBe(true);
});
it("should not wait on newer tasks for the same draft", () => {
const A = new BaseDraftTask('localid-A');
A.sequentialId = 1;
const B = new BaseDraftTask('localid-A');
B.sequentialId = 2;
expect(A.isDependentOnTask(B)).toBe(false)
});
it("should not wait on older tasks for other drafts", () => {
const A = new BaseDraftTask('localid-other');
A.sequentialId = 1;
const B = new BaseDraftTask('localid-A');
B.sequentialId = 2;
expect(A.isDependentOnTask(B)).toBe(false);
expect(B.isDependentOnTask(A)).toBe(false);
});
});
describe("performLocal", () => {
it("rejects if we we don't pass a draft", () => {
const badTask = new BaseDraftTask(null)
badTask.performLocal().then(() => {
throw new Error("Shouldn't succeed")
}).catch((err) => {
expect(err.message).toBe("Attempt to call BaseDraftTask.performLocal without a headerMessageId")
});
});
});
describe("refreshDraftReference", () => {
it("should retrieve the draft by client ID, with the body, and assign it to @draft", () => {
const draft = new Message({draft: true});
const A = new BaseDraftTask('localid-other');
spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(draft));
waitsForPromise(() => {
return A.refreshDraftReference().then((resolvedValue) => {
expect(A.draft).toEqual(draft);
expect(resolvedValue).toEqual(draft);
const query = DatabaseStore.run.mostRecentCall.args[0];
expect(query.sql()).toEqual("SELECT `Message`.`data`, IFNULL(`MessageBody`.`value`, '!NULLVALUE!') AS `body` FROM `Message` LEFT OUTER JOIN `MessageBody` ON `MessageBody`.`id` = `Message`.`id` WHERE `Message`.`client_id` = 'localid-other' ORDER BY `Message`.`date` ASC LIMIT 1");
});
});
});
it("should throw a DraftNotFoundError error if it the response was no longer a draft", () => {
const message = new Message({draft: false});
const A = new BaseDraftTask('localid-other');
spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(message));
waitsForPromise(() => {
return A.refreshDraftReference().then(() => {
throw new Error("Should not have resolved");
}).catch((err) => {
expect(err instanceof BaseDraftTask.DraftNotFoundError).toBe(true);
})
});
});
it("should throw a DraftNotFoundError error if nothing was returned", () => {
const A = new BaseDraftTask('localid-other');
spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(null));
waitsForPromise(() => {
return A.refreshDraftReference().then(() => {
throw new Error("Should not have resolved");
}).catch((err) => {
expect(err instanceof BaseDraftTask.DraftNotFoundError).toBe(true);
})
});
});
});
});

View file

@ -80,7 +80,8 @@ function InflatesDraftClientId(ComposedComponent) {
return;
}
if (this.state.draft.pristine) {
Actions.destroyDraft(this.props.headerMessageId);
const {accountId, headerMessageId} = this.state.draft;
Actions.destroyDraft(accountId, headerMessageId);
}
}

View file

@ -7,8 +7,8 @@ import DraftFactory from './draft-factory';
import DatabaseStore from './database-store';
import SendActionsStore from './send-actions-store';
import FocusedContentStore from './focused-content-store';
import BaseDraftTask from '../tasks/base-draft-task';
import SyncbackDraftTask from '../tasks/syncback-draft-task';
import SendDraftTask from '../tasks/send-draft-task';
import DestroyDraftTask from '../tasks/destroy-draft-task';
import Thread from '../models/thread';
import Message from '../models/message';
@ -127,7 +127,7 @@ class DraftStore extends NylasStore {
_.each(this._draftSessions, (session) => {
const draft = session.draft()
if (draft && draft.pristine) {
Actions.queueTask(new DestroyDraftTask(session.headerMessageId));
Actions.queueTask(new DestroyDraftTask(draft.accountId, draft.headerMessageId));
} else {
promises.push(session.changes.commit());
}
@ -335,7 +335,7 @@ class DraftStore extends NylasStore {
});
}
_onDestroyDraft = (headerMessageId) => {
_onDestroyDraft = (accountId, headerMessageId) => {
const session = this._draftSessions[headerMessageId];
// Immediately reset any pending changes so no saves occur
@ -345,13 +345,16 @@ class DraftStore extends NylasStore {
// Stop any pending tasks related ot the draft
TaskQueue.queue().forEach((task) => {
if (task instanceof BaseDraftTask && task.headerMessageId === headerMessageId) {
if (task instanceof SyncbackDraftTask && task.headerMessageId === headerMessageId) {
Actions.dequeueTask(task.id);
}
if (task instanceof SendDraftTask && task.headerMessageId === headerMessageId) {
Actions.dequeueTask(task.id);
}
})
// Queue the task to destroy the draft
Actions.queueTask(new DestroyDraftTask(headerMessageId));
Actions.queueTask(new DestroyDraftTask(accountId, headerMessageId));
if (NylasEnv.isComposerWindow()) {
NylasEnv.close();

View file

@ -13,7 +13,7 @@ const DefaultSendAction = {
iconUrl: null,
configKey: DefaultSendActionKey,
isAvailableForDraft: () => true,
performSendAction: ({draft}) => Actions.queueTask(new SendDraftTask(draft.id)),
performSendAction: ({draft}) => Actions.queueTask(new SendDraftTask(draft)),
}
function verifySendAction(sendAction = {}, extension = {}) {

View file

@ -1,10 +0,0 @@
import Task from './task';
import DraftHelpers from '../stores/draft-helpers';
export default class BaseDraftTask extends Task {
constructor(draft) {
super();
this.draft = draft;
}
}

View file

@ -1,8 +1,9 @@
import BaseDraftTask from './base-draft-task';
import Task from './task';
export default class DestroyDraftTask extends BaseDraftTask {
constructor(headerMessageId) {
export default class DestroyDraftTask extends Task {
constructor(accountId, headerMessageId) {
super();
this.accountId = accountId;
this.headerMessageId = headerMessageId;
}
}

View file

@ -1,27 +1,19 @@
/* eslint global-require: 0 */
import Task from './task';
import Actions from '../actions';
import Message from '../models/message';
import Account from '../models/account';
import NylasAPI from '../nylas-api';
import * as NylasAPIHelpers from '../nylas-api-helpers';
import {APIError, RequestEnsureOnceError} from '../errors';
import SoundRegistry from '../../registries/sound-registry';
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 EnsureMessageInSentFolderTask from './ensure-message-in-sent-folder-task';
import Task from './task';
const OPEN_TRACKING_ID = NylasEnv.packages.pluginIdFor('open-tracking')
const LINK_TRACKING_ID = NylasEnv.packages.pluginIdFor('link-tracking')
export default class SendDraftTask extends BaseDraftTask {
constructor(headerMessageId, {playSound = true, emitError = true, allowMultiSend = true} = {}) {
super(headerMessageId);
this.draft = null;
this.message = null;
export default class SendDraftTask extends Task {
constructor(draft, {playSound = true, emitError = true, allowMultiSend = true} = {}) {
super();
this.draft = draft;
this.accountId = (draft || {}).accountId;
this.headerMessageId = (draft || {}).headerMessageId;
this.emitError = emitError
this.playSound = playSound
this.allowMultiSend = allowMultiSend
@ -31,16 +23,6 @@ export default class SendDraftTask extends BaseDraftTask {
return "Sending message";
}
performRemote() {
return this.refreshDraftReference()
.then(this.assertDraftValidity)
.then(this.sendMessage)
.then(this.ensureInSentFolder)
.then(this.updatePluginMetadata)
.then(this.onSuccess)
.catch(this.onError);
}
assertDraftValidity = () => {
if (!this.draft.from[0]) {
return Promise.reject(new Error("SendDraftTask - you must populate `from` before sending."));
@ -64,99 +46,6 @@ export default class SendDraftTask extends BaseDraftTask {
return (!!this.draft.metadataForPluginId(OPEN_TRACKING_ID) || !!this.draft.metadataForPluginId(LINK_TRACKING_ID)) || false;
}
hasCustomBodyPerRecipient = () => {
if (!this.allowMultiSend) {
return false;
}
// Sending individual bodies for too many participants can cause us
// to hit the smtp rate limit.
const participants = this.draft.participants({includeFrom: false, includeBcc: true})
if (participants.length === 1 || participants.length > 10) {
return false;
}
const providerCompatible = (AccountStore.accountForId(this.draft.accountId).provider !== "eas");
return this._trackingPluginsInUse() && providerCompatible;
}
sendMessage = async () => {
if (this.hasCustomBodyPerRecipient()) {
await this._sendPerRecipient();
} else {
await this._sendWithSingleBody()
}
}
ensureInSentFolder = () => {
const t = new EnsureMessageInSentFolderTask({
message: this.message,
customSentMessage: this.hasCustomBodyPerRecipient() || this._trackingPluginsInUse(),
})
Actions.queueTask(t)
}
_sendWithSingleBody = async () => {
let responseJSON = {}
if (this._syncbackRequestId) {
responseJSON = await SyncbackTaskAPIRequest.waitForQueuedRequest(this._syncbackRequestId)
} else {
const task = new SyncbackTaskAPIRequest({
api: NylasAPI,
options: {
path: "/send",
accountId: this.draft.accountId,
method: 'POST',
body: this.draft.toJSON(),
timeout: 1000 * 60 * 5, // We cannot hang up a send - won't know if it sent
requestId: this.draft.id,
onSyncbackRequestCreated: (syncbackRequest) => {
this._syncbackRequestId = syncbackRequest.id
},
},
})
responseJSON = await task.run();
}
await this._createMessageFromResponse(responseJSON)
}
_sendPerRecipient = async () => {
let responseJSON = {}
if (this._syncbackRequestId) {
responseJSON = await SyncbackTaskAPIRequest.waitForQueuedRequest(this._syncbackRequestId)
} else {
const task = new SyncbackTaskAPIRequest({
api: NylasAPI,
options: {
path: "/send-per-recipient",
accountId: this.draft.accountId,
method: 'POST',
body: {
message: this.draft.toJSON(),
uses_open_tracking: this.draft.metadataForPluginId(OPEN_TRACKING_ID) != null,
uses_link_tracking: this.draft.metadataForPluginId(LINK_TRACKING_ID) != null,
},
timeout: 1000 * 60 * 5, // We cannot hang up a send - won't know if it sent
onSyncbackRequestCreated: (syncbackRequest) => {
this._syncbackRequestId = syncbackRequest.id
},
},
})
responseJSON = await task.run();
}
await this._createMessageFromResponse(responseJSON);
}
updatePluginMetadata = () => {
this.message.pluginMetadata.forEach((m) => {
const t1 = new SyncbackMetadataTask(this.message.id,
this.message.constructor.name, m.pluginId);
Actions.queueTask(t1);
});
return Promise.resolve();
}
_createMessageFromResponse = (responseJSON) => {
const {failedRecipients, message} = responseJSON
if (failedRecipients && failedRecipients.length > 0) {
@ -201,10 +90,6 @@ export default class SendDraftTask extends BaseDraftTask {
}
onError = (err) => {
if (err instanceof BaseDraftTask.DraftNotFoundError) {
return Promise.resolve(Task.Status.Continue);
}
let message = err.message;
// TODO Handle errors in a cleaner way
@ -259,4 +144,5 @@ export default class SendDraftTask extends BaseDraftTask {
return Promise.resolve([Task.Status.Failed, err]);
}
}

View file

@ -1,5 +1,10 @@
import BaseDraftTask from './base-draft-task';
export default class SyncbackDraftTask extends BaseDraftTask {
import Task from './task';
export default class SyncbackDraftTask extends Task {
constructor(draft) {
super();
this.draft = draft;
this.accountId = (draft || {}).accountId;
this.headerMessageId = (draft || {}).headerMessageId;
}
}

View file

@ -371,7 +371,7 @@ export default class NylasEnvConstructor {
if (event.defaultPrevented) { return; }
this.lastUncaughtError = error;
try {
extra.pluginIds = this._findPluginsFromError(error);
} catch (err) {