mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-03-03 03:23:45 +08:00
[local-sync] feat(send): Add support for attachments
Also move some helper function logic onto the Message model
This commit is contained in:
parent
8307976df8
commit
56f8d41b8c
4 changed files with 130 additions and 94 deletions
|
@ -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());
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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} = {}) {
|
||||
|
|
Loading…
Reference in a new issue