[local-sync] feat(send): Add support for attachments

Also move some helper function logic onto the Message model
This commit is contained in:
Halla Moore 2016-12-07 14:06:07 -08:00
parent 8307976df8
commit 56f8d41b8c
4 changed files with 130 additions and 94 deletions

View file

@ -81,7 +81,13 @@ module.exports = (server) => {
try {
const accountId = request.auth.credentials.id;
const db = await LocalDatabaseConnector.forAccount(accountId)
const draft = await SendingUtils.findOrCreateMessageFromJSON(request.payload, db, false)
const draftData = Object.assign(request.payload, {
unread: true,
is_draft: false,
is_sent: false,
version: 0,
})
const draft = await SendingUtils.findOrCreateMessageFromJSON(draftData, db)
await (draft.isSending = true);
const savedDraft = await draft.save();
reply(savedDraft.toJSON());

View file

@ -1,16 +1,3 @@
const _ = require('underscore');
const setReplyHeaders = (newMessage, prevMessage) => {
if (prevMessage.messageIdHeader) {
newMessage.inReplyTo = prevMessage.headerMessageId;
if (prevMessage.references) {
newMessage.references = prevMessage.references.concat(prevMessage.headerMessageId);
} else {
newMessage.references = [prevMessage.messageIdHeader];
}
}
}
class HTTPError extends Error {
constructor(message, httpCode, logContext) {
super(message);
@ -21,90 +8,25 @@ class HTTPError extends Error {
module.exports = {
HTTPError,
findOrCreateMessageFromJSON: async (data, db, isDraft) => {
const {Thread, Message} = db;
setReplyHeaders: (newMessage, prevMessage) => {
if (prevMessage.messageIdHeader) {
newMessage.inReplyTo = prevMessage.headerMessageId;
if (prevMessage.references) {
newMessage.references = prevMessage.references.concat(prevMessage.headerMessageId);
} else {
newMessage.references = [prevMessage.messageIdHeader];
}
}
},
findOrCreateMessageFromJSON: async (data, db) => {
const {Message} = db;
const existingMessage = await Message.findById(data.id);
if (existingMessage) {
return existingMessage;
}
const {to, cc, bcc, from, replyTo, subject, body, account_id, date, id} = data;
const message = Message.build({
accountId: account_id,
from: from,
to: to,
cc: cc,
bcc: bcc,
replyTo: replyTo,
subject: subject,
body: body,
unread: true,
isDraft: isDraft,
isSent: false,
version: 0,
date: date,
id: id,
});
// TODO
// Attach files
// Update our contact list
// Add events
// Add metadata??
let replyToThread;
let replyToMessage;
if (data.thread_id != null) {
replyToThread = await Thread.find({
where: {id: data.thread_id},
include: [{
model: Message,
as: 'messages',
attributes: _.without(Object.keys(Message.attributes), 'body'),
}],
});
}
if (data.reply_to_message_id != null) {
replyToMessage = await Message.findById(data.reply_to_message_id);
}
if (replyToThread && replyToMessage) {
if (!replyToThread.messages.find((msg) => msg.id === replyToMessage.id)) {
throw new HTTPError(
`Message ${replyToMessage.id} is not in thread ${replyToThread.id}`,
400
)
}
}
let thread;
if (replyToMessage) {
setReplyHeaders(message, replyToMessage);
thread = await message.getThread();
} else if (replyToThread) {
thread = replyToThread;
const previousMessages = thread.messages.filter(msg => !msg.isDraft);
if (previousMessages.length > 0) {
const lastMessage = previousMessages[previousMessages.length - 1]
setReplyHeaders(message, lastMessage);
}
} else {
thread = Thread.build({
accountId: account_id,
subject: message.subject,
firstMessageDate: message.date,
lastMessageDate: message.date,
lastMessageSentDate: message.date,
})
}
const savedMessage = await message.save();
const savedThread = await thread.save();
await savedThread.addMessage(savedMessage);
return savedMessage;
return Message.associateFromJSON(data, db)
},
findMultiSendDraft: async (draftId, db) => {
const draft = await db.Message.findById(draftId)

View file

@ -1,3 +1,4 @@
const fs = require('fs');
const nodemailer = require('nodemailer');
const mailcomposer = require('mailcomposer');
const {HTTPError} = require('./sending-utils');
@ -42,10 +43,18 @@ class SendmailClient {
}
}
this._logger.error('Max sending retries reached');
this._handleError(error);
}
_handleError(err) {
// TODO: figure out how to parse different errors, like in cloud-core
// https://github.com/nylas/cloud-core/blob/production/sync-engine/inbox/sendmail/smtp/postel.py#L354
throw new HTTPError('Sending failed', 500, error)
if (err.startsWith("Error: Invalid login: 535-5.7.8 Username and Password not accepted.")) {
throw new HTTPError('Invalid login', 401, err)
}
throw new HTTPError('Sending failed', 500, err);
}
_draftToMsgData(draft) {
@ -59,7 +68,14 @@ class SendmailClient {
msgData.html = draft.body;
msgData.messageId = `${draft.id}@nylas.com`;
// TODO: attachments
msgData.attachments = []
for (const upload of draft.uploads) {
msgData.attachments.push({
filename: upload.filename,
content: fs.createReadStream(upload.targetPath),
cid: upload.id,
})
}
if (draft.replyTo) {
msgData.replyTo = formatParticipants(draft.replyTo);

View file

@ -1,3 +1,4 @@
const _ = require('underscore');
const cryptography = require('crypto');
const {PromiseUtils, IMAPConnection} = require('isomorphic-core')
const {DatabaseTypes: {buildJSONColumnOptions, buildJSONARRAYColumnOptions}} = require('isomorphic-core');
@ -71,6 +72,21 @@ module.exports = (sequelize, Sequelize) => {
this.setDataValue('isSending', val);
},
},
uploads: Object.assign(buildJSONARRAYColumnOptions('testFiles'), {
validate: {
uploadStructure: function uploadStructure(stringifiedArr) {
const arr = JSON.parse(stringifiedArr);
const requiredKeys = ['filename', 'targetPath', 'id']
arr.forEach((upload) => {
requiredKeys.forEach((key) => {
if (!upload.hasOwnPropery(key)) {
throw new Error(`Upload must have '${key}' key.`)
}
})
})
},
},
}),
}, {
indexes: [
{
@ -89,6 +105,82 @@ module.exports = (sequelize, Sequelize) => {
hashForHeaders(headers) {
return cryptography.createHash('sha256').update(headers, 'utf8').digest('hex');
},
fromJSON(data) {
// TODO: events, metadata??
return this.build({
accountId: data.account_id,
from: data.from,
to: data.to,
cc: data.cc,
bcc: data.bcc,
replyTo: data.reply_to,
subject: data.subject,
body: data.body,
unread: true,
isDraft: data.is_draft,
isSent: false,
version: 0,
date: data.date,
id: data.id,
uploads: data.uploads,
});
},
async associateFromJSON(data, db) {
const message = this.fromJSON(data);
const {Thread, Message} = db;
let replyToThread;
let replyToMessage;
if (data.thread_id != null) {
replyToThread = await Thread.find({
where: {id: data.thread_id},
include: [{
model: Message,
as: 'messages',
attributes: _.without(Object.keys(Message.attributes), 'body'),
}],
});
}
if (data.reply_to_message_id != null) {
replyToMessage = await Message.findById(data.reply_to_message_id);
}
if (replyToThread && replyToMessage) {
if (!replyToThread.messages.find((msg) => msg.id === replyToMessage.id)) {
throw new SendingUtils.HTTPError(
`Message ${replyToMessage.id} is not in thread ${replyToThread.id}`,
400
)
}
}
let thread;
if (replyToMessage) {
SendingUtils.setReplyHeaders(message, replyToMessage);
thread = await message.getThread();
} else if (replyToThread) {
thread = replyToThread;
const previousMessages = thread.messages.filter(msg => !msg.isDraft);
if (previousMessages.length > 0) {
const lastMessage = previousMessages[previousMessages.length - 1]
SendingUtils.setReplyHeaders(message, lastMessage);
}
} else {
thread = Thread.build({
accountId: message.accountId,
subject: message.subject,
firstMessageDate: message.date,
lastMessageDate: message.date,
lastMessageSentDate: message.date,
})
}
const savedMessage = await message.save();
const savedThread = await thread.save();
await savedThread.addMessage(savedMessage);
return savedMessage;
},
},
instanceMethods: {
async setLabelsFromXGM(xGmLabels, {Label, preloadedLabels} = {}) {