mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-11 01:54:40 +08:00
feat(multi-send): Allow multi-send for non-Exchange accounts
Summary: Updates the send task to use multi-send for emails with link/open tracking metadata sent via SMTP. Places an email without link/open tracking in the sent folder. TODO: Override send button (i.e., mail merge) and move all of the multi-send tasks to package. Test Plan: Tested locally. Reviewers: evan, bengotow, juan Reviewed By: bengotow, juan Differential Revision: https://phab.nylas.com/D2974
This commit is contained in:
parent
3211301b66
commit
c3d8ab5ceb
4 changed files with 212 additions and 28 deletions
40
src/flux/tasks/multi-send-session-close-task.es6
Normal file
40
src/flux/tasks/multi-send-session-close-task.es6
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Task from './task';
|
||||
import {APIError} from '../errors';
|
||||
import NylasAPI from '../nylas-api';
|
||||
import MultiSendToIndividualTask from './multi-send-to-individual-task';
|
||||
|
||||
|
||||
export default class MultiSendSessionCloseTask extends Task {
|
||||
constructor(opts = {}) {
|
||||
super(opts);
|
||||
this.message = opts.message;
|
||||
}
|
||||
|
||||
isDependentOnTask(other) {
|
||||
return (other instanceof MultiSendToIndividualTask) && (other.message.clientId === this.message.clientId);
|
||||
}
|
||||
|
||||
performRemote() {
|
||||
return NylasAPI.makeRequest({
|
||||
method: "DELETE",
|
||||
path: `/send-multiple/${this.message.id}`,
|
||||
accountId: this.message.accountId,
|
||||
})
|
||||
.then(() => {
|
||||
return Promise.resolve(Task.Status.Success);
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMessage = `We had trouble saving this message to your Sent folder.\n\n${err.message}`;
|
||||
if (err instanceof APIError) {
|
||||
if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {
|
||||
NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true});
|
||||
return Promise.resolve([Task.Status.Failed, err]);
|
||||
}
|
||||
return Promise.resolve(Task.Status.Retry);
|
||||
}
|
||||
NylasEnv.reportError(err);
|
||||
NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true});
|
||||
return Promise.resolve([Task.Status.Failed, err]);
|
||||
});
|
||||
}
|
||||
}
|
57
src/flux/tasks/multi-send-to-individual-task.es6
Normal file
57
src/flux/tasks/multi-send-to-individual-task.es6
Normal file
|
@ -0,0 +1,57 @@
|
|||
import Task from './task';
|
||||
import {APIError} from '../errors';
|
||||
import NylasAPI from '../nylas-api';
|
||||
import {RegExpUtils} from 'nylas-exports';
|
||||
|
||||
|
||||
export default class MultiSendToIndividualTask extends Task {
|
||||
constructor(opts = {}) {
|
||||
super(opts);
|
||||
this.message = opts.message;
|
||||
this.recipient = opts.recipient;
|
||||
}
|
||||
|
||||
performRemote() {
|
||||
return NylasAPI.makeRequest({
|
||||
method: "POST",
|
||||
path: `/send-multiple/${this.message.id}`,
|
||||
accountId: this.message.accountId,
|
||||
body: {
|
||||
send_to: {
|
||||
email: this.recipient.email,
|
||||
name: this.recipient.name,
|
||||
},
|
||||
body: this._customizeTrackingForRecipient(this.message.body),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Promise.resolve(Task.Status.Success);
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMessage = `We had trouble sending this message. ${this.recipient.displayName()} may not have received this email.\n\n${err.message}`;
|
||||
if (err instanceof APIError) {
|
||||
if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {
|
||||
NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true});
|
||||
return Promise.resolve([Task.Status.Failed, err]);
|
||||
}
|
||||
return Promise.resolve(Task.Status.Retry);
|
||||
}
|
||||
NylasEnv.reportError(err);
|
||||
NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true});
|
||||
return Promise.resolve([Task.Status.Failed, err]);
|
||||
});
|
||||
}
|
||||
|
||||
_customizeTrackingForRecipient(text) {
|
||||
const encodedEmail = btoa(this.recipient.email)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
let body = text.replace(/<img class="n1-open"[^<]+src="([a-zA-Z0-9-_:\/.]*)">/g, (match, url) => {
|
||||
return `<img class="n1-open" width="0" height="0" style="border:0; width:0; height:0;" src="${url}?r=${encodedEmail}">`;
|
||||
});
|
||||
body = body.replace(RegExpUtils.urlLinkTagRegex(), (match, prefix, url, suffix, content, closingTag) => {
|
||||
return `${prefix}${url}&r=${encodedEmail}${suffix}${content}${closingTag}`;
|
||||
});
|
||||
return body;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint global-require: 0 */
|
||||
import {RegExpUtils} from 'nylas-exports';
|
||||
import Task from './task';
|
||||
import Actions from '../actions';
|
||||
import Message from '../models/message';
|
||||
|
@ -7,8 +9,18 @@ import SoundRegistry from '../../sound-registry';
|
|||
import DatabaseStore from '../stores/database-store';
|
||||
import AccountStore from '../stores/account-store';
|
||||
import BaseDraftTask from './base-draft-task';
|
||||
import MultiSendToIndividualTask from './multi-send-to-individual-task';
|
||||
import MultiSendSessionCloseTask from './multi-send-session-close-task';
|
||||
import SyncbackMetadataTask from './syncback-metadata-task';
|
||||
import NotifyPluginsOfSendTask from './notify-plugins-of-send-task';
|
||||
let OPEN_TRACKING_ID = null;
|
||||
let LINK_TRACKING_ID = null;
|
||||
try {
|
||||
OPEN_TRACKING_ID = require('../../../internal_packages/open-tracking/lib/open-tracking-constants').PLUGIN_ID;
|
||||
LINK_TRACKING_ID = require('../../../internal_packages/link-tracking/lib/link-tracking-constants').PLUGIN_ID;
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
export default class SendDraftTask extends BaseDraftTask {
|
||||
|
||||
|
@ -27,18 +39,6 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
return this.refreshDraftReference()
|
||||
.then(this.assertDraftValidity)
|
||||
.then(this.sendMessage)
|
||||
.then((responseJSON) => {
|
||||
this.message = new Message().fromJSON(responseJSON)
|
||||
this.message.clientId = this.draft.clientId
|
||||
this.message.draft = false
|
||||
this.message.clonePluginMetadataFrom(this.draft)
|
||||
|
||||
return DatabaseStore.inTransaction((t) =>
|
||||
this.refreshDraftReference().then(() =>
|
||||
t.persistModel(this.message)
|
||||
)
|
||||
);
|
||||
})
|
||||
.then(this.updatePluginMetadata)
|
||||
.then(this.onSuccess)
|
||||
.catch(this.onError);
|
||||
|
@ -62,9 +62,55 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
sendMessage = () => {
|
||||
if (OPEN_TRACKING_ID && LINK_TRACKING_ID &&
|
||||
(this.draft.metadataForPluginId(OPEN_TRACKING_ID) ||
|
||||
this.draft.metadataForPluginId(LINK_TRACKING_ID)) &&
|
||||
AccountStore.accountForId(this.draft.accountId).provider !== "eas") {
|
||||
return this.sendWithMultipleBodies();
|
||||
}
|
||||
return this.sendWithSingleBody();
|
||||
}
|
||||
|
||||
sendWithMultipleBodies = () => {
|
||||
const draft = this.draft.clone();
|
||||
draft.body = this.stripTrackingFromBody(draft.body);
|
||||
return NylasAPI.makeRequest({
|
||||
path: "/send-multiple",
|
||||
accountId: this.draft.accountId,
|
||||
method: 'POST',
|
||||
body: draft.toJSON(),
|
||||
timeout: 1000 * 60 * 5, // We cannot hang up a send - won't know if it sent
|
||||
returnsModel: false,
|
||||
})
|
||||
.catch((err) => {
|
||||
this.onSendError(err, this.sendWithMultipleBodies);
|
||||
})
|
||||
.then((responseJSON) => {
|
||||
return this.createMessageFromResponse(responseJSON);
|
||||
})
|
||||
.then(() => {
|
||||
const recipients = this.message.to.concat(this.message.cc, this.message.bcc);
|
||||
recipients.forEach((recipient) => {
|
||||
const t1 = new MultiSendToIndividualTask({
|
||||
message: this.message,
|
||||
recipient: recipient,
|
||||
});
|
||||
Actions.queueTask(t1);
|
||||
});
|
||||
const t2 = new MultiSendSessionCloseTask({
|
||||
message: this.message,
|
||||
});
|
||||
Actions.queueTask(t2);
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
// This function returns a promise that resolves to the draft when the draft has
|
||||
// been sent successfully.
|
||||
sendMessage = () => {
|
||||
sendWithSingleBody = () => {
|
||||
return NylasAPI.makeRequest({
|
||||
path: "/send",
|
||||
accountId: this.draft.accountId,
|
||||
|
@ -74,20 +120,13 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
returnsModel: false,
|
||||
})
|
||||
.catch((err) => {
|
||||
// If the message you're "replying to" were deleted
|
||||
if (err.message && err.message.indexOf('Invalid message public id') === 0) {
|
||||
this.draft.replyToMessageId = null
|
||||
return this.sendMessage()
|
||||
}
|
||||
|
||||
// If the thread was deleted
|
||||
if (err.message && err.message.indexOf('Invalid thread') === 0) {
|
||||
this.draft.threadId = null;
|
||||
this.draft.replyToMessageId = null;
|
||||
return this.sendMessage();
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
this.onSendError(err, this.sendWithSingleBody);
|
||||
})
|
||||
.then((responseJSON) => {
|
||||
return this.createMessageFromResponse(responseJSON);
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -107,7 +146,36 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
Actions.queueTask(t2);
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
createMessageFromResponse = (responseJSON) => {
|
||||
this.message = new Message().fromJSON(responseJSON);
|
||||
this.message.clientId = this.draft.clientId;
|
||||
this.message.body = this.draft.body;
|
||||
this.message.draft = false;
|
||||
this.message.clonePluginMetadataFrom(this.draft);
|
||||
|
||||
return DatabaseStore.inTransaction((t) =>
|
||||
this.refreshDraftReference().then(() => {
|
||||
return t.persistModel(this.message);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
stripTrackingFromBody(text) {
|
||||
let body = text.replace(/<img class="n1-open"[^<]+src="([a-zA-Z0-9-_:\/.]*)">/g, () => {
|
||||
return "";
|
||||
});
|
||||
body = body.replace(RegExpUtils.urlLinkTagRegex(), (match, prefix, url, suffix, content, closingTag) => {
|
||||
const param = url.split("?")[1];
|
||||
if (param) {
|
||||
const link = decodeURIComponent(param.split("=")[1]);
|
||||
return `${prefix}${link}${suffix}${content}${closingTag}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
return body;
|
||||
}
|
||||
|
||||
onSuccess = () => {
|
||||
|
@ -156,4 +224,21 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
|
||||
return Promise.resolve([Task.Status.Failed, err]);
|
||||
}
|
||||
|
||||
onSendError = (err, retrySend) => {
|
||||
// If the message you're "replying to" has been deleted
|
||||
if (err.message && err.message.indexOf('Invalid message public id') === 0) {
|
||||
this.draft.replyToMessageId = null;
|
||||
return retrySend();
|
||||
}
|
||||
|
||||
// If the thread has been deleted
|
||||
if (err.message && err.message.indexOf('Invalid thread') === 0) {
|
||||
this.draft.threadId = null;
|
||||
this.draft.replyToMessageId = null;
|
||||
return retrySend();
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,6 +90,8 @@ class NylasExports
|
|||
@lazyLoad "TaskFactory", 'flux/tasks/task-factory'
|
||||
@lazyLoadAndRegisterTask "EventRSVPTask", 'event-rsvp-task'
|
||||
@lazyLoadAndRegisterTask "SendDraftTask", 'send-draft-task'
|
||||
@lazyLoadAndRegisterTask "MultiSendToIndividualTask", 'multi-send-to-individual-task'
|
||||
@lazyLoadAndRegisterTask "MultiSendSessionCloseTask", 'multi-send-session-close-task'
|
||||
@lazyLoadAndRegisterTask "ChangeMailTask", 'change-mail-task'
|
||||
@lazyLoadAndRegisterTask "DestroyDraftTask", 'destroy-draft-task'
|
||||
@lazyLoadAndRegisterTask "ChangeLabelsTask", 'change-labels-task'
|
||||
|
|
Loading…
Reference in a new issue