wildduck/lib/message-handler.js

1684 lines
71 KiB
JavaScript
Raw Normal View History

'use strict';
2017-07-12 02:38:23 +08:00
const crypto = require('crypto');
2020-05-08 15:43:59 +08:00
const { v1: uuidV1 } = require('uuid');
const ObjectID = require('mongodb').ObjectID;
const Indexer = require('../imap-core/lib/indexer/indexer');
const ImapNotifier = require('./imap-notifier');
2017-08-07 02:25:10 +08:00
const AttachmentStorage = require('./attachment-storage');
2019-10-01 16:22:10 +08:00
const AuditHandler = require('./audit-handler');
2017-04-02 00:22:47 +08:00
const libmime = require('libmime');
const counters = require('./counters');
2017-07-21 02:33:41 +08:00
const consts = require('./consts');
2017-07-17 03:40:34 +08:00
const tools = require('./tools');
const openpgp = require('openpgp');
2017-06-08 21:04:34 +08:00
const parseDate = require('../imap-core/lib/parse-date');
// index only the following headers for SEARCH
2018-09-25 16:01:56 +08:00
const INDEXED_HEADERS = ['to', 'cc', 'subject', 'from', 'sender', 'reply-to', 'message-id', 'thread-index', 'list-id'];
2017-04-09 17:33:10 +08:00
2018-01-02 21:04:01 +08:00
openpgp.config.commentstring = 'Plaintext message encrypted by WildDuck Mail Server';
class MessageHandler {
constructor(options) {
this.database = options.database;
this.redis = options.redis;
2017-08-07 02:25:10 +08:00
2018-10-18 15:37:32 +08:00
this.loggelf = options.loggelf || (() => false);
2017-08-07 02:25:10 +08:00
this.attachmentStorage =
options.attachmentStorage ||
new AttachmentStorage({
gridfs: options.gridfs || options.database,
options: options.attachments,
redis: this.redis
2017-08-07 02:25:10 +08:00
});
this.indexer = new Indexer({
attachmentStorage: this.attachmentStorage,
loggelf: message => this.loggelf(message)
});
2017-08-07 02:25:10 +08:00
this.notifier = new ImapNotifier({
database: options.database,
2017-07-17 21:32:31 +08:00
redis: this.redis,
pushOnly: true
});
2017-08-07 02:25:10 +08:00
2017-07-31 06:11:45 +08:00
this.users = options.users || options.database;
this.counters = counters(this.redis);
2019-10-01 16:22:10 +08:00
this.auditHandler = new AuditHandler({
database: this.database,
users: this.users,
gridfs: options.gridfs || this.database,
bucket: 'audit',
loggelf: message => this.loggelf(message)
});
}
getMailbox(options, callback) {
2017-11-17 19:37:53 +08:00
let query = options.query;
if (!query) {
query = {};
if (options.mailbox) {
2018-04-29 03:44:38 +08:00
if (tools.isId(options.mailbox._id)) {
2017-11-17 19:37:53 +08:00
return setImmediate(() => callback(null, options.mailbox));
}
2018-04-29 03:44:38 +08:00
if (tools.isId(options.mailbox)) {
query._id = new ObjectID(options.mailbox);
} else {
return callback(new Error('Invalid mailbox ID'));
}
2017-11-17 19:37:53 +08:00
if (options.user) {
query.user = options.user;
}
2017-04-11 05:36:22 +08:00
} else {
2017-11-17 19:37:53 +08:00
query.user = options.user;
if (options.specialUse) {
query.specialUse = options.specialUse;
2018-10-11 16:48:12 +08:00
} else if (options.path) {
2017-11-17 19:37:53 +08:00
query.path = options.path;
2018-10-11 16:48:12 +08:00
} else {
let err = new Error('Mailbox is missing');
err.imapResponse = 'TRYCREATE';
return callback(err);
2017-11-17 19:37:53 +08:00
}
2017-04-11 05:36:22 +08:00
}
}
2017-04-11 05:36:22 +08:00
2019-03-20 21:00:57 +08:00
this.database.collection('mailboxes').findOne(query, (err, mailboxData) => {
if (err) {
return callback(err);
}
2019-03-20 21:00:57 +08:00
if (!mailboxData) {
if (options.path !== 'INBOX' && options.inboxDefault) {
return this.database.collection('mailboxes').findOne(
{
user: options.user,
path: 'INBOX'
},
(err, mailboxData) => {
if (err) {
return callback(err);
}
if (!mailboxData) {
let err = new Error('Mailbox is missing');
err.imapResponse = 'TRYCREATE';
return callback(err);
}
callback(null, mailboxData);
}
);
}
let err = new Error('Mailbox is missing');
err.imapResponse = 'TRYCREATE';
return callback(err);
}
2019-03-20 21:00:57 +08:00
callback(null, mailboxData);
});
}
2017-04-11 05:36:22 +08:00
// Monster method for inserting new messages to a mailbox
// TODO: Refactor into smaller pieces
add(options, callback) {
2018-12-04 20:45:41 +08:00
if (!options.prepared && options.raw && options.raw.length > consts.MAX_ALLOWED_MESSAGE_SIZE) {
2017-11-10 21:04:58 +08:00
return setImmediate(() => callback(new Error('Message size ' + options.raw.length + ' bytes is too large')));
}
this.prepareMessage(options, (err, prepared) => {
if (err) {
return callback(err);
}
2017-11-10 21:04:58 +08:00
let id = prepared.id;
let mimeTree = prepared.mimeTree;
let size = prepared.size;
let bodystructure = prepared.bodystructure;
let envelope = prepared.envelope;
let idate = prepared.idate;
let hdate = prepared.hdate;
let msgid = prepared.msgid;
let subject = prepared.subject;
let headers = prepared.headers;
let flags = Array.isArray(options.flags) ? options.flags : [].concat(options.flags || []);
let maildata = options.maildata || this.indexer.getMaildata(mimeTree);
this.getMailbox(options, (err, mailboxData) => {
if (err) {
return callback(err);
}
2017-05-15 21:09:08 +08:00
2018-10-23 16:38:08 +08:00
let cleanup = (...args) => {
if (!args[0]) {
return callback(...args);
}
2017-05-15 21:09:08 +08:00
2018-10-23 16:38:08 +08:00
let attachmentIds = Object.keys(mimeTree.attachmentMap || {}).map(key => mimeTree.attachmentMap[key]);
if (!attachmentIds.length) {
return callback(...args);
}
2017-05-15 21:09:08 +08:00
2018-10-23 16:38:08 +08:00
this.attachmentStorage.deleteMany(attachmentIds, maildata.magic, () => callback(...args));
};
2018-10-23 16:38:08 +08:00
this.indexer.storeNodeBodies(maildata, mimeTree, err => {
if (err) {
return cleanup(err);
}
2017-04-10 22:12:47 +08:00
2018-10-23 16:38:08 +08:00
// prepare message object
let messageData = {
_id: id,
2017-05-23 22:17:54 +08:00
2018-10-23 16:38:08 +08:00
// should be kept when COPY'ing or MOVE'ing
root: id,
2017-07-12 02:38:23 +08:00
2018-10-23 16:38:08 +08:00
v: consts.SCHEMA_VERSION,
2018-10-23 16:38:08 +08:00
// if true then expires after rdate + retention
exp: !!mailboxData.retention,
rdate: Date.now() + (mailboxData.retention || 0),
2017-04-10 22:12:47 +08:00
2018-10-23 16:38:08 +08:00
// make sure the field exists. it is set to true when user is deleted
userDeleted: false,
2017-04-10 22:12:47 +08:00
2018-10-23 16:38:08 +08:00
idate,
hdate,
flags,
size,
2017-11-17 19:37:53 +08:00
2018-10-23 16:38:08 +08:00
// some custom metadata about the delivery
meta: options.meta || {},
2017-04-24 19:51:11 +08:00
2018-10-23 16:38:08 +08:00
// list filter IDs that matched this message
filters: Array.isArray(options.filters) ? options.filters : [].concat(options.filters || []),
2017-04-10 22:12:47 +08:00
2018-10-23 16:38:08 +08:00
headers,
mimeTree,
envelope,
bodystructure,
msgid,
2017-05-15 21:09:08 +08:00
2018-10-23 16:38:08 +08:00
// use boolean for more commonly used (and searched for) flags
unseen: !flags.includes('\\Seen'),
flagged: flags.includes('\\Flagged'),
undeleted: !flags.includes('\\Deleted'),
draft: flags.includes('\\Draft'),
2017-04-10 22:12:47 +08:00
2018-10-23 16:38:08 +08:00
magic: maildata.magic,
2017-07-12 02:38:23 +08:00
2018-12-03 20:50:32 +08:00
subject,
// do not archive deleted messages that have been copied
copied: false
2018-10-23 16:38:08 +08:00
};
2019-01-18 17:23:31 +08:00
if (options.verificationResults) {
messageData.verificationResults = options.verificationResults;
}
2018-10-23 16:38:08 +08:00
if (options.outbound) {
messageData.outbound = [].concat(options.outbound || []);
}
2017-10-20 18:43:44 +08:00
2018-10-23 16:38:08 +08:00
if (options.forwardTargets) {
messageData.forwardTargets = [].concat(options.forwardTargets || []);
}
2017-10-27 20:45:51 +08:00
2018-10-23 16:38:08 +08:00
if (maildata.attachments && maildata.attachments.length) {
messageData.attachments = maildata.attachments;
2020-03-19 19:21:05 +08:00
messageData.ha = maildata.attachments.some(a => !a.related);
2018-10-23 16:38:08 +08:00
} else {
messageData.ha = false;
}
2017-11-10 21:04:58 +08:00
2018-10-23 16:38:08 +08:00
if (maildata.text) {
messageData.text = maildata.text.replace(/\r\n/g, '\n').trim();
2018-10-23 16:38:08 +08:00
// text is indexed with a fulltext index, so only store the beginning of it
if (messageData.text.length > consts.MAX_PLAINTEXT_INDEXED) {
messageData.textFooter = messageData.text.substr(consts.MAX_PLAINTEXT_INDEXED);
messageData.text = messageData.text.substr(0, consts.MAX_PLAINTEXT_INDEXED);
2017-11-08 22:22:07 +08:00
2018-10-23 16:38:08 +08:00
// truncate remaining text if total length exceeds maximum allowed
if (
consts.MAX_PLAINTEXT_CONTENT > consts.MAX_PLAINTEXT_INDEXED &&
messageData.textFooter.length > consts.MAX_PLAINTEXT_CONTENT - consts.MAX_PLAINTEXT_INDEXED
) {
messageData.textFooter = messageData.textFooter.substr(0, consts.MAX_PLAINTEXT_CONTENT - consts.MAX_PLAINTEXT_INDEXED);
}
}
messageData.text =
messageData.text.length <= consts.MAX_PLAINTEXT_CONTENT
? messageData.text
: messageData.text.substr(0, consts.MAX_PLAINTEXT_CONTENT);
messageData.intro = messageData.text
// assume we get the intro text from first 2 kB
.substr(0, 2 * 1024)
2020-05-15 22:49:38 +08:00
// remove markdown urls
.replace(/\[[^\]]*\]/g, ' ')
2018-10-23 16:38:08 +08:00
// remove quoted parts
// "> quote from previous message"
.replace(/^>.*$/gm, '')
// remove lines with repetetive chars
// "---------------------"
.replace(/^\s*(.)\1+\s*$/gm, '')
// join lines
.replace(/\s+/g, ' ')
.trim();
2018-12-03 17:06:47 +08:00
2018-10-23 16:38:08 +08:00
if (messageData.intro.length > 128) {
let intro = messageData.intro.substr(0, 128);
let lastSp = intro.lastIndexOf(' ');
if (lastSp > 0) {
intro = intro.substr(0, lastSp);
}
messageData.intro = intro + '…';
}
}
2017-11-08 22:22:07 +08:00
2018-10-23 16:38:08 +08:00
if (maildata.html && maildata.html.length) {
let htmlSize = 0;
messageData.html = maildata.html
.map(html => {
if (htmlSize >= consts.MAX_HTML_CONTENT || !html) {
return '';
}
2018-10-23 16:38:08 +08:00
if (htmlSize + Buffer.byteLength(html) <= consts.MAX_HTML_CONTENT) {
htmlSize += Buffer.byteLength(html);
return html;
}
2017-11-10 21:04:58 +08:00
2018-10-23 16:38:08 +08:00
html = html.substr(0, htmlSize + Buffer.byteLength(html) - consts.MAX_HTML_CONTENT);
htmlSize += Buffer.byteLength(html);
return html;
})
.filter(html => html);
}
2018-11-30 17:16:14 +08:00
this.users.collection('users').findOneAndUpdate(
2018-10-23 16:38:08 +08:00
{
_id: mailboxData.user
},
{
$inc: {
storageUsed: size
2017-03-27 04:58:05 +08:00
}
2018-10-23 16:38:08 +08:00
},
2018-11-30 17:16:14 +08:00
{
returnOriginal: false,
projection: {
storageUsed: true
}
},
(err, r) => {
2018-10-23 16:38:08 +08:00
if (err) {
return cleanup(err);
}
2018-11-30 17:16:14 +08:00
if (r && r.value) {
this.loggelf({
short_message: '[QUOTA] +',
2018-12-03 17:06:47 +08:00
_mail_action: 'quota',
2018-11-30 17:16:14 +08:00
_user: mailboxData.user,
_inc: size,
2018-12-03 17:06:47 +08:00
_storage_used: r.value.storageUsed,
2020-03-24 21:39:04 +08:00
_sess: options.session && options.session.id,
2018-12-03 17:29:41 +08:00
_mailbox: mailboxData._id
2018-11-30 17:16:14 +08:00
});
}
2018-10-23 16:38:08 +08:00
let rollback = err => {
2018-11-30 17:16:14 +08:00
this.users.collection('users').findOneAndUpdate(
2018-10-23 16:38:08 +08:00
{
_id: mailboxData.user
},
{
$inc: {
storageUsed: -size
}
},
2018-11-30 17:16:14 +08:00
{
returnOriginal: false,
projection: {
storageUsed: true
}
},
(...args) => {
let r = args && args[1];
if (r && r.value) {
this.loggelf({
short_message: '[QUOTA] -',
2018-12-03 17:06:47 +08:00
_mail_action: 'quota',
2018-11-30 17:16:14 +08:00
_user: mailboxData.user,
_inc: -size,
2018-12-03 17:06:47 +08:00
_storage_used: r.value.storageUsed,
2020-03-24 21:39:04 +08:00
_sess: options.session && options.session.id,
2018-12-03 17:29:41 +08:00
_mailbox: mailboxData._id,
2018-11-30 17:16:14 +08:00
_rollback: 'yes',
_error: err.message,
_code: err.code
});
}
2018-10-23 16:38:08 +08:00
cleanup(err);
}
);
};
2018-10-23 16:38:08 +08:00
// acquire new UID+MODSEQ
this.database.collection('mailboxes').findOneAndUpdate(
2017-11-22 22:22:36 +08:00
{
2018-10-23 16:38:08 +08:00
_id: mailboxData._id
2017-11-22 22:22:36 +08:00
},
{
2017-11-10 21:04:58 +08:00
$inc: {
2018-10-23 16:38:08 +08:00
// allocate bot UID and MODSEQ values so when journal is later sorted by
// modseq then UIDs are always in ascending order
uidNext: 1,
modifyIndex: 1
2017-11-10 21:04:58 +08:00
}
2017-11-22 22:22:36 +08:00
},
2018-10-23 16:38:08 +08:00
{
// use original value to get correct UIDNext
returnOriginal: true
},
(err, item) => {
2017-11-10 21:04:58 +08:00
if (err) {
2018-10-23 16:38:08 +08:00
return rollback(err);
2017-11-10 21:04:58 +08:00
}
2018-10-23 16:38:08 +08:00
if (!item || !item.value) {
// was not able to acquire a lock
let err = new Error('Mailbox is missing');
err.imapResponse = 'TRYCREATE';
return rollback(err);
}
2018-10-23 16:38:08 +08:00
let mailboxData = item.value;
2017-11-10 21:04:58 +08:00
2018-10-23 16:38:08 +08:00
// updated message object by setting mailbox specific values
messageData.mailbox = mailboxData._id;
messageData.user = mailboxData.user;
messageData.uid = mailboxData.uidNext;
messageData.modseq = mailboxData.modifyIndex + 1;
2017-10-27 16:50:37 +08:00
2018-10-23 16:38:08 +08:00
if (!['\\Junk', '\\Trash'].includes(mailboxData.specialUse) && !flags.includes('\\Deleted')) {
messageData.searchable = true;
}
2017-11-12 21:13:32 +08:00
2018-10-23 16:38:08 +08:00
if (mailboxData.specialUse === '\\Junk') {
messageData.junk = true;
}
2017-11-10 21:04:58 +08:00
2018-10-23 16:38:08 +08:00
this.getThreadId(mailboxData.user, subject, mimeTree, (err, thread) => {
if (err) {
return rollback(err);
2017-11-22 22:22:36 +08:00
}
2017-05-23 22:17:54 +08:00
2018-10-23 16:38:08 +08:00
messageData.thread = thread;
2018-11-30 17:16:14 +08:00
this.database.collection('messages').insertOne(messageData, { w: 'majority' }, (err, r) => {
2018-10-23 16:38:08 +08:00
if (err) {
return rollback(err);
}
2018-11-30 17:16:14 +08:00
if (!r || !r.insertedCount) {
let err = new Error('Failed to store message');
err.code = 'StoreError';
return rollback(err);
}
2018-10-23 16:38:08 +08:00
let logTime = messageData.meta.time || new Date();
if (typeof logTime === 'number') {
logTime = new Date(logTime);
}
2018-10-23 15:56:42 +08:00
2018-10-23 16:38:08 +08:00
let uidValidity = mailboxData.uidValidity;
let uid = messageData.uid;
2017-11-22 22:22:36 +08:00
2018-10-23 16:38:08 +08:00
if (
options.session &&
options.session.selected &&
options.session.selected.mailbox &&
options.session.selected.mailbox.toString() === mailboxData._id.toString()
) {
options.session.writeStream.write(options.session.formatResponse('EXISTS', messageData.uid));
}
2018-01-23 19:38:49 +08:00
2018-10-23 16:38:08 +08:00
this.notifier.addEntries(
mailboxData,
{
command: 'EXISTS',
uid: messageData.uid,
ignore: options.session && options.session.id,
message: messageData._id,
modseq: messageData.modseq,
unseen: messageData.unseen
},
() => {
this.notifier.fire(mailboxData.user);
2019-10-01 16:22:10 +08:00
let raw = options.rawchunks || options.raw;
let processAudits = async () => {
2020-07-01 15:37:28 +08:00
let audits = await this.database
.collection('audits')
.find({ user: mailboxData.user, expires: { $gt: new Date() } })
.toArray();
2019-10-01 16:22:10 +08:00
let now = new Date();
for (let auditData of audits) {
if ((auditData.start && auditData.start > now) || (auditData.end && auditData.end < now)) {
// audit not active
continue;
}
await this.auditHandler.store(auditData._id, raw, {
date: messageData.idate,
msgid: messageData.msgid,
header: messageData.mimeTree && messageData.mimeTree.parsedHeader,
ha: messageData.ha,
mailbox: mailboxData._id,
mailboxPath: mailboxData.path,
2020-05-27 18:59:23 +08:00
info: Object.assign({ queueId: messageData.outbound }, messageData.meta)
2019-10-01 16:22:10 +08:00
});
}
};
let next = () => {
cleanup(null, true, {
2019-10-01 16:22:10 +08:00
uidValidity,
uid,
id: messageData._id,
mailbox: mailboxData._id,
mailboxPath: mailboxData.path,
2019-10-01 16:22:10 +08:00
status: 'new'
});
};
// do not use more suitable .finally() as it is not supported in Node v8
2020-05-08 15:43:59 +08:00
return processAudits().then(next).catch(next);
2018-10-23 16:38:08 +08:00
}
);
});
});
}
);
}
2018-01-23 19:38:49 +08:00
);
});
2018-10-23 16:38:08 +08:00
});
2018-01-23 19:38:49 +08:00
});
2017-05-23 22:17:54 +08:00
}
2018-11-30 17:16:14 +08:00
updateQuota(user, inc, options, callback) {
inc = inc || {};
if (options.delayNotifications) {
// quota change is handled at some later time
return callback();
}
2018-11-30 17:16:14 +08:00
this.users.collection('users').findOneAndUpdate(
{
2018-11-26 16:02:39 +08:00
_id: user
},
{
$inc: {
storageUsed: Number(inc.storageUsed) || 0
}
},
2018-11-30 17:16:14 +08:00
{
returnOriginal: false,
projection: {
storageUsed: true
}
},
(...args) => {
let r = args && args[1];
if (r && r.value) {
this.loggelf({
short_message: '[QUOTA] ' + (Number(inc.storageUsed) || 0 < 0 ? '-' : '+'),
2018-12-03 17:06:47 +08:00
_mail_action: 'quota',
2018-11-30 17:16:14 +08:00
_user: user,
_inc: inc.storageUsed,
2018-12-03 17:06:47 +08:00
_storage_used: r.value.storageUsed,
2020-03-24 21:39:04 +08:00
_sess: options.session && options.session.id,
2018-12-03 17:29:41 +08:00
_mailbox: inc.mailbox
2018-11-30 17:16:14 +08:00
});
}
callback(...args);
}
);
}
2017-04-10 22:12:47 +08:00
del(options, callback) {
2017-11-17 19:37:53 +08:00
let messageData = options.messageData;
2017-11-22 22:22:36 +08:00
let curtime = new Date();
this.getMailbox(
2017-07-21 21:29:57 +08:00
options.mailbox || {
2017-10-20 18:43:44 +08:00
mailbox: messageData.mailbox
},
(err, mailboxData) => {
2017-11-17 19:37:53 +08:00
if (err && !err.imapResponse) {
return callback(err);
}
2017-11-17 19:37:53 +08:00
let pushToArchive = next => {
if (!options.archive) {
2017-11-22 22:22:36 +08:00
return next(null, false);
}
2018-12-03 20:50:32 +08:00
2017-11-22 22:22:36 +08:00
messageData.archived = curtime;
2017-11-17 19:37:53 +08:00
messageData.exp = true;
2017-11-22 22:22:36 +08:00
messageData.rdate = curtime.getTime() + consts.ARCHIVE_TIME;
2018-12-03 21:03:34 +08:00
this.database.collection('archived').insertOne(messageData, { w: 'majority' }, (err, r) => {
2017-11-17 19:37:53 +08:00
if (err) {
if (err.code === 11000) {
// already archived, probably the same message from another mailbox
return next(null, true);
}
2017-11-17 19:37:53 +08:00
return callback(err);
}
2018-10-19 17:19:43 +08:00
2018-12-03 21:03:34 +08:00
if (r && r.insertedCount) {
this.loggelf({
short_message: '[ARCHIVED]',
_mail_action: 'archived',
_user: messageData.user,
_mailbox: messageData.mailbox,
_uid: messageData.uid,
2020-05-08 15:43:59 +08:00
_stored_id: messageData._id,
2018-12-03 21:03:34 +08:00
_expires: messageData.rdate,
2020-03-24 21:39:04 +08:00
_sess: options.session && options.session.id,
2018-12-03 21:03:34 +08:00
_size: messageData.size
});
}
2018-10-19 17:19:43 +08:00
return next(null, true);
2017-11-17 19:37:53 +08:00
});
};
2018-10-19 17:19:43 +08:00
pushToArchive(err => {
2017-11-22 22:22:36 +08:00
if (err) {
return callback(err);
}
2018-11-26 16:02:39 +08:00
2017-11-22 22:22:36 +08:00
this.database.collection('messages').deleteOne(
{
_id: messageData._id,
mailbox: messageData.mailbox,
uid: messageData.uid
},
2018-10-25 00:21:10 +08:00
{ w: 'majority' },
2018-11-28 16:36:30 +08:00
(err, r) => {
2017-11-22 22:22:36 +08:00
if (err) {
return callback(err);
}
2017-05-15 21:09:08 +08:00
2018-11-28 16:36:30 +08:00
if (!r || !r.deletedCount) {
// nothing was deleted!
return callback(null, false);
}
2017-11-22 22:22:36 +08:00
this.updateQuota(
2018-11-26 16:02:39 +08:00
messageData.user,
2017-11-22 22:22:36 +08:00
{
2018-12-03 17:29:41 +08:00
storageUsed: -messageData.size,
mailbox: messageData.mailbox
2017-11-22 22:22:36 +08:00
},
2018-11-30 17:16:14 +08:00
options,
2017-11-22 22:22:36 +08:00
() => {
if (!mailboxData) {
// deleted an orphan message
return callback(null, true);
2017-11-17 19:37:53 +08:00
}
2017-05-15 21:09:08 +08:00
2017-11-22 22:22:36 +08:00
let updateAttachments = next => {
if (options.archive) {
// archived messages still need the attachments
return next();
}
2017-11-22 22:22:36 +08:00
let attachmentIds = Object.keys(messageData.mimeTree.attachmentMap || {}).map(
key => messageData.mimeTree.attachmentMap[key]
);
2018-11-26 16:02:39 +08:00
2017-11-22 22:22:36 +08:00
if (!attachmentIds.length) {
return next();
}
2017-11-22 22:22:36 +08:00
this.attachmentStorage.deleteMany(attachmentIds, messageData.magic, next);
};
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
updateAttachments(() => {
if (
options.session &&
options.session.selected &&
options.session.selected.mailbox &&
options.session.selected.mailbox.toString() === mailboxData._id.toString()
) {
2017-11-22 22:22:36 +08:00
options.session.writeStream.write(options.session.formatResponse('EXPUNGE', messageData.uid));
}
2017-11-22 22:22:36 +08:00
this.notifier.addEntries(
mailboxData,
2017-11-22 22:22:36 +08:00
{
command: 'EXPUNGE',
ignore: options.session && options.session.id,
uid: messageData.uid,
message: messageData._id,
unseen: messageData.unseen
},
() => {
if (!options.delayNotifications) {
this.notifier.fire(mailboxData.user);
2017-11-22 22:22:36 +08:00
}
2018-10-19 17:19:43 +08:00
return callback(null, true);
2017-11-22 22:22:36 +08:00
}
);
});
}
);
}
);
});
}
);
}
2017-04-09 17:33:10 +08:00
move(options, callback) {
this.getMailbox(options.source, (err, mailboxData) => {
2017-04-09 17:33:10 +08:00
if (err) {
return callback(err);
}
this.getMailbox(options.destination, (err, targetData) => {
2017-04-09 17:33:10 +08:00
if (err) {
return callback(err);
}
2017-11-22 22:22:36 +08:00
this.database.collection('mailboxes').findOneAndUpdate(
{
_id: mailboxData._id
},
{
$inc: {
// increase the mailbox modification index
// to indicate that something happened
modifyIndex: 1
}
},
{
2018-01-04 18:15:08 +08:00
returnOriginal: false,
projection: {
_id: true,
2020-01-27 20:22:35 +08:00
uidNext: true,
modifyIndex: true
2018-01-04 18:15:08 +08:00
}
2017-11-22 22:22:36 +08:00
},
2018-01-04 18:15:08 +08:00
(err, item) => {
if (err) {
return callback(err);
}
let newModseq = (item && item.value && item.value.modifyIndex) || 1;
2017-11-22 22:22:36 +08:00
let cursor = this.database
.collection('messages')
.find({
2017-08-24 21:27:53 +08:00
mailbox: mailboxData._id,
2017-11-22 22:22:36 +08:00
uid: options.messageQuery ? options.messageQuery : tools.checkRangeQuery(options.messages)
})
// ordering is needed for IMAP UIDPLUS results
.sort({ uid: 1 });
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
let sourceUid = [];
let destinationUid = [];
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
let removeEntries = [];
let existsEntries = [];
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
let done = err => {
let next = () => {
if (err) {
return callback(err);
}
return callback(null, true, {
uidValidity: targetData.uidValidity,
2017-11-22 22:22:36 +08:00
sourceUid,
destinationUid,
mailbox: mailboxData._id,
2019-01-09 05:27:03 +08:00
target: targetData._id,
2017-11-22 22:22:36 +08:00
status: 'moved'
});
};
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
if (sourceUid.length && options.showExpunged) {
options.session.writeStream.write({
tag: '*',
command: String(options.session.selected.uidList.length),
attributes: [
{
type: 'atom',
value: 'EXISTS'
}
]
});
2017-04-09 17:33:10 +08:00
}
2017-11-22 22:22:36 +08:00
if (existsEntries.length) {
// mark messages as deleted from old mailbox
return this.notifier.addEntries(mailboxData, removeEntries, () => {
2017-11-22 22:22:36 +08:00
// mark messages as added to new mailbox
this.notifier.addEntries(targetData, existsEntries, () => {
this.notifier.fire(mailboxData.user);
2017-11-22 22:22:36 +08:00
next();
});
});
2017-07-21 21:29:57 +08:00
}
2017-11-22 22:22:36 +08:00
next();
};
let processNext = () => {
cursor.next((err, message) => {
2017-04-09 17:33:10 +08:00
if (err) {
2017-11-22 22:22:36 +08:00
return done(err);
2017-04-09 17:33:10 +08:00
}
2017-11-22 22:22:36 +08:00
if (!message) {
return cursor.close(done);
2017-04-09 17:33:10 +08:00
}
2017-11-22 22:22:36 +08:00
let messageId = message._id;
let messageUid = message.uid;
2017-04-09 17:33:10 +08:00
2017-07-21 21:29:57 +08:00
if (options.returnIds) {
2017-11-22 22:22:36 +08:00
sourceUid.push(message._id);
2017-07-21 21:29:57 +08:00
} else {
2017-11-22 22:22:36 +08:00
sourceUid.push(messageUid);
2017-07-21 21:29:57 +08:00
}
2017-07-16 00:40:08 +08:00
2017-11-22 22:22:36 +08:00
this.database.collection('mailboxes').findOneAndUpdate(
{
_id: targetData._id
2017-11-22 22:22:36 +08:00
},
{
$inc: {
uidNext: 1
}
},
{
2018-09-25 15:05:34 +08:00
projection: {
uidNext: true,
modifyIndex: true
},
returnOriginal: true
2017-11-22 22:22:36 +08:00
},
(err, item) => {
if (err) {
return cursor.close(() => done(err));
}
2017-11-22 22:22:36 +08:00
if (!item || !item.value) {
return cursor.close(() => done(new Error('Mailbox disappeared')));
}
2017-11-22 22:22:36 +08:00
message._id = new ObjectID();
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
let uidNext = item.value.uidNext;
2018-09-25 15:05:34 +08:00
let modifyIndex = item.value.modifyIndex;
2017-07-20 21:10:36 +08:00
2017-11-22 22:22:36 +08:00
if (options.returnIds) {
destinationUid.push(message._id);
} else {
destinationUid.push(uidNext);
}
2017-11-22 22:22:36 +08:00
// set new mailbox
message.mailbox = targetData._id;
2017-11-22 22:22:36 +08:00
// new mailbox means new UID
message.uid = uidNext;
2017-07-21 21:29:57 +08:00
2017-11-22 22:22:36 +08:00
// retention settings
message.exp = !!targetData.retention;
message.rdate = Date.now() + (targetData.retention || 0);
2018-09-25 15:05:34 +08:00
message.modseq = modifyIndex; // reset message modseq to whatever it is for the mailbox right now
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
let unseen = message.unseen;
2017-04-09 17:33:10 +08:00
message.searchable = true;
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
let junk = false;
if (targetData.specialUse === '\\Junk' && !message.junk) {
2017-11-22 22:22:36 +08:00
message.junk = true;
junk = 1;
} else if (targetData.specialUse !== '\\Trash' && message.junk) {
2017-11-22 22:22:36 +08:00
delete message.junk;
junk = -1;
}
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
Object.keys(options.updates || []).forEach(key => {
switch (key) {
case 'seen':
case 'deleted':
{
let fname = '\\' + key.charAt(0).toUpperCase() + key.substr(1);
if (!options.updates[key] && !message.flags.includes(fname)) {
// add missing flag
message.flags.push(fname);
} else if (options.updates[key] && message.flags.includes(fname)) {
// remove non-needed flag
let flags = new Set(message.flags);
flags.delete(fname);
message.flags = Array.from(flags);
}
message['un' + key] = options.updates[key];
}
break;
case 'flagged':
case 'draft':
{
let fname = '\\' + key.charAt(0).toUpperCase() + key.substr(1);
if (options.updates[key] && !message.flags.includes(fname)) {
// add missing flag
message.flags.push(fname);
} else if (!options.updates[key] && message.flags.includes(fname)) {
// remove non-needed flag
let flags = new Set(message.flags);
flags.delete(fname);
message.flags = Array.from(flags);
}
message[key] = options.updates[key];
}
break;
case 'expires':
{
if (options.updates.expires) {
message.exp = true;
message.rdate = options.updates.expires.getTime();
} else {
message.exp = false;
}
}
break;
2018-12-04 16:50:27 +08:00
case 'metaData':
message.meta = message.meta || {};
message.meta.custom = options.updates.metaData;
break;
2017-11-22 22:22:36 +08:00
}
2017-04-09 17:33:10 +08:00
});
2017-11-22 22:22:36 +08:00
if (options.markAsSeen) {
message.unseen = false;
if (!message.flags.includes('\\Seen')) {
message.flags.push('\\Seen');
}
2017-11-17 19:37:53 +08:00
}
2018-10-25 00:21:10 +08:00
this.database.collection('messages').insertOne(message, { w: 'majority' }, (err, r) => {
2017-11-22 22:22:36 +08:00
if (err) {
return cursor.close(() => done(err));
}
2018-11-30 17:16:14 +08:00
if (!r || !r.insertedCount) {
let err = new Error('Failed to store message');
err.code = 'StoreError';
return cursor.close(() => done(err));
}
2017-11-22 22:22:36 +08:00
let insertId = r.insertedId;
// delete old message
this.database.collection('messages').deleteOne(
{
_id: messageId,
mailbox: mailboxData._id,
uid: messageUid
},
2018-10-25 00:21:10 +08:00
{ w: 'majority' },
(err, r) => {
2017-11-22 22:22:36 +08:00
if (err) {
return cursor.close(() => done(err));
}
if (r && r.deletedCount) {
if (options.session) {
options.session.writeStream.write(options.session.formatResponse('EXPUNGE', sourceUid));
}
2017-11-22 22:22:36 +08:00
removeEntries.push({
command: 'EXPUNGE',
ignore: options.session && options.session.id,
uid: messageUid,
message: messageId,
unseen,
// modseq is needed to avoid updating mailbox entry
modseq: newModseq
});
if (options.showExpunged) {
options.session.writeStream.write(options.session.formatResponse('EXPUNGE', messageUid));
}
2017-11-22 22:22:36 +08:00
}
let entry = {
command: 'EXISTS',
uid: uidNext,
message: insertId,
unseen: message.unseen
};
if (junk) {
entry.junk = junk;
}
existsEntries.push(entry);
if (existsEntries.length >= consts.BULK_BATCH_SIZE) {
// mark messages as deleted from old mailbox
return this.notifier.addEntries(mailboxData, removeEntries, () => {
2017-11-22 22:22:36 +08:00
// mark messages as added to new mailbox
this.notifier.addEntries(targetData, existsEntries, () => {
2017-11-22 22:22:36 +08:00
removeEntries = [];
existsEntries = [];
this.notifier.fire(mailboxData.user);
2017-11-22 22:22:36 +08:00
processNext();
});
});
}
processNext();
2017-11-22 22:22:36 +08:00
}
);
});
}
);
2017-04-09 17:33:10 +08:00
});
2017-11-22 22:22:36 +08:00
};
2017-04-09 17:33:10 +08:00
2017-11-22 22:22:36 +08:00
processNext();
}
);
2017-04-09 17:33:10 +08:00
});
});
}
2017-04-13 16:35:39 +08:00
2018-11-26 16:02:39 +08:00
// NB! does not update user quota
2017-11-17 19:37:53 +08:00
put(messageData, callback) {
let getMailbox = next => {
this.getMailbox({ mailbox: messageData.mailbox }, (err, mailboxData) => {
2018-10-11 16:48:12 +08:00
if (err && err.imapResponse !== 'TRYCREATE') {
2017-11-17 19:37:53 +08:00
return callback(err);
}
2018-10-11 16:48:12 +08:00
2017-11-17 19:37:53 +08:00
if (mailboxData) {
return next(null, mailboxData);
}
2018-10-11 16:48:12 +08:00
2017-11-17 19:37:53 +08:00
this.getMailbox(
{
query: {
user: messageData.user,
path: 'INBOX'
}
},
2019-06-07 20:41:21 +08:00
next
2017-11-17 19:37:53 +08:00
);
});
};
getMailbox((err, mailboxData) => {
if (err) {
return callback(err);
}
2017-11-22 22:22:36 +08:00
this.database.collection('mailboxes').findOneAndUpdate(
{
_id: mailboxData._id
},
{
$inc: {
uidNext: 1
}
},
{
uidNext: true
},
(err, item) => {
if (err) {
return callback(err);
}
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
if (!item || !item.value) {
return callback(new Error('Mailbox disappeared'));
}
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
let uidNext = item.value.uidNext;
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
// set new mailbox
messageData.mailbox = mailboxData._id;
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
// new mailbox means new UID
messageData.uid = uidNext;
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
// this will be changed later by the notification system
messageData.modseq = 0;
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
// retention settings
messageData.exp = !!mailboxData.retention;
messageData.rdate = Date.now() + (mailboxData.retention || 0);
2017-11-17 19:37:53 +08:00
if (!mailboxData.undeleted) {
2017-11-22 22:22:36 +08:00
delete messageData.searchable;
} else {
messageData.searchable = true;
}
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
let junk = false;
if (mailboxData.specialUse === '\\Junk' && !messageData.junk) {
messageData.junk = true;
junk = 1;
} else if (mailboxData.specialUse !== '\\Trash' && messageData.junk) {
delete messageData.junk;
junk = -1;
2017-11-17 19:37:53 +08:00
}
2018-10-25 00:21:10 +08:00
this.database.collection('messages').insertOne(messageData, { w: 'majority' }, (err, r) => {
2017-11-22 22:22:36 +08:00
if (err) {
if (err.code === 11000) {
// message already exists
return callback(null, false);
}
2017-11-22 22:22:36 +08:00
return callback(err);
}
2017-11-17 19:37:53 +08:00
2018-11-30 17:16:14 +08:00
if (!r || !r.insertedCount) {
let err = new Error('Failed to store message');
err.code = 'StoreError';
return callback(err);
}
2017-11-22 22:22:36 +08:00
let insertId = r.insertedId;
let entry = {
command: 'EXISTS',
uid: uidNext,
message: insertId,
unseen: messageData.unseen
};
if (junk) {
entry.junk = junk;
}
// mark messages as added to new mailbox
this.notifier.addEntries(mailboxData, entry, () => {
this.notifier.fire(mailboxData.user);
2017-11-22 22:22:36 +08:00
return callback(null, {
mailbox: mailboxData._id,
2019-05-21 19:00:47 +08:00
message: insertId,
2017-11-22 22:22:36 +08:00
uid: uidNext
});
2017-11-17 19:37:53 +08:00
});
});
2017-11-22 22:22:36 +08:00
}
);
2017-11-17 19:37:53 +08:00
});
}
2018-10-22 17:00:24 +08:00
generateIndexedHeaders(headersArray) {
// allow configuring extra header keys that are indexed
return (headersArray || [])
.map(line => {
line = Buffer.from(line, 'binary').toString();
2017-04-13 16:35:39 +08:00
2020-05-08 15:43:59 +08:00
let key = line.substr(0, line.indexOf(':')).trim().toLowerCase();
2017-05-23 22:17:54 +08:00
2018-10-22 17:00:24 +08:00
if (!INDEXED_HEADERS.includes(key)) {
// do not index this header
return false;
}
2017-05-23 22:17:54 +08:00
2017-09-01 19:50:53 +08:00
let value = line
.substr(line.indexOf(':') + 1)
.trim()
.replace(/\s*\r?\n\s*/g, ' ');
2017-04-13 16:35:39 +08:00
try {
value = libmime.decodeWords(value);
} catch (E) {
// ignore
}
2017-04-13 16:35:39 +08:00
// store indexed value as lowercase for easier SEARCHing
value = value.toLowerCase();
2017-04-13 16:35:39 +08:00
2018-09-25 16:01:56 +08:00
switch (key) {
case 'list-id':
// only index the actual ID of the list
if (value.indexOf('<') >= 0) {
let m = value.match(/<([^>]+)/);
if (m && m[1] && m[1].trim()) {
value = m[1].trim();
}
}
break;
}
// trim long values as mongodb indexed fields can not be too long
if (Buffer.byteLength(key, 'utf-8') >= 255) {
2020-05-08 15:43:59 +08:00
key = Buffer.from(key).slice(0, 255).toString();
key = key.substr(0, key.length - 4);
}
2017-04-13 16:35:39 +08:00
if (Buffer.byteLength(value, 'utf-8') >= 880) {
// value exceeds MongoDB max indexed value length
2020-05-08 15:43:59 +08:00
value = Buffer.from(value).slice(0, 880).toString();
// remove last 4 chars to be sure we do not have any incomplete unicode sequences
value = value.substr(0, value.length - 4);
}
2017-04-13 16:35:39 +08:00
return {
key,
value
};
})
.filter(line => line);
2017-04-13 16:35:39 +08:00
}
2017-11-10 21:04:58 +08:00
prepareMessage(options, callback) {
if (options.prepared) {
return setImmediate(() => callback(null, options.prepared));
}
2017-04-13 16:35:39 +08:00
let id = new ObjectID();
let mimeTree = options.mimeTree || this.indexer.parseMimeTree(options.raw);
2017-04-13 16:35:39 +08:00
let size = this.indexer.getSize(mimeTree);
let bodystructure = this.indexer.getBodyStructure(mimeTree);
let envelope = this.indexer.getEnvelope(mimeTree);
2017-06-08 21:04:34 +08:00
let idate = (options.date && parseDate(options.date)) || new Date();
let hdate = (mimeTree.parsedHeader.date && parseDate([].concat(mimeTree.parsedHeader.date || []).pop() || '', idate)) || false;
2017-04-13 16:35:39 +08:00
let subject = ([].concat(mimeTree.parsedHeader.subject || []).pop() || '').trim();
2017-07-12 02:38:23 +08:00
try {
subject = libmime.decodeWords(subject);
} catch (E) {
// ignore
}
2018-11-02 16:18:24 +08:00
subject = this.normalizeSubject(subject, {
2018-11-13 20:11:48 +08:00
removePrefix: false
2018-11-02 16:18:24 +08:00
});
2017-07-12 02:38:23 +08:00
2017-04-13 16:35:39 +08:00
let flags = [].concat(options.flags || []);
if (!hdate || hdate.toString() === 'Invalid Date') {
hdate = idate;
}
let msgid = envelope[9] || '<' + uuidV1() + '@wildduck.email>';
2017-04-13 16:35:39 +08:00
2018-10-22 17:00:24 +08:00
let headers = this.generateIndexedHeaders(mimeTree.header);
2017-04-13 16:35:39 +08:00
2017-11-10 21:04:58 +08:00
let prepared = {
2017-04-13 16:35:39 +08:00
id,
mimeTree,
size,
bodystructure,
envelope,
idate,
hdate,
flags,
msgid,
2017-07-12 02:38:23 +08:00
headers,
subject
2017-04-13 16:35:39 +08:00
};
2017-11-10 21:04:58 +08:00
return setImmediate(() => callback(null, prepared));
2017-04-13 16:35:39 +08:00
}
2017-07-12 02:38:23 +08:00
// resolves or generates new thread id for a message
getThreadId(userId, subject, mimeTree, callback) {
let referenceIds = new Set(
[
[].concat(mimeTree.parsedHeader['message-id'] || []).pop() || '',
[].concat(mimeTree.parsedHeader['in-reply-to'] || []).pop() || '',
([].concat(mimeTree.parsedHeader['thread-index'] || []).pop() || '').substr(0, 22),
[].concat(mimeTree.parsedHeader.references || []).pop() || ''
2017-07-12 02:38:23 +08:00
]
.join(' ')
.split(/\s+/)
.map(id => id.replace(/[<>]/g, '').trim())
.filter(id => id)
2020-05-08 15:43:59 +08:00
.map(id => crypto.createHash('sha1').update(id).digest('base64').replace(/[=]+$/g, ''))
2017-07-12 02:38:23 +08:00
);
2018-11-13 20:11:48 +08:00
subject = this.normalizeSubject(subject, {
removePrefix: true
});
2017-07-12 02:38:23 +08:00
referenceIds = Array.from(referenceIds).slice(0, 10);
// most messages are not threaded, so an upsert call should be ok to make
2017-11-22 22:22:36 +08:00
this.database.collection('threads').findOneAndUpdate(
{
2017-07-12 02:38:23 +08:00
user: userId,
2017-11-22 22:22:36 +08:00
ids: { $in: referenceIds },
subject
},
{
$addToSet: {
ids: { $each: referenceIds }
},
$set: {
updated: new Date()
}
},
{
returnOriginal: false
},
(err, r) => {
2017-07-12 02:38:23 +08:00
if (err) {
return callback(err);
}
2017-11-22 22:22:36 +08:00
if (r.value) {
return callback(null, r.value._id);
}
// thread not found, create a new one
this.database.collection('threads').insertOne(
{
user: userId,
subject,
ids: referenceIds,
updated: new Date()
},
(err, r) => {
if (err) {
return callback(err);
}
return callback(null, r.insertedId);
}
);
}
);
2017-07-12 02:38:23 +08:00
}
2018-11-02 16:18:24 +08:00
normalizeSubject(subject, options) {
options = options || {};
subject = subject.replace(/\s+/g, ' ').trim();
if (options.removePrefix) {
let match = true;
while (match) {
match = false;
subject = subject
.replace(/^(re|fwd?)\s*:|\s*\(fwd\)\s*$/gi, () => {
match = true;
return '';
})
.trim();
}
2017-07-12 02:38:23 +08:00
}
return subject;
}
update(user, mailbox, messageQuery, changes, callback) {
let updates = { $set: {} };
let update = false;
let addFlags = [];
let removeFlags = [];
let notifyEntries = [];
Object.keys(changes || {}).forEach(key => {
switch (key) {
case 'seen':
updates.$set.unseen = !changes.seen;
if (changes.seen) {
addFlags.push('\\Seen');
} else {
removeFlags.push('\\Seen');
}
update = true;
break;
case 'deleted':
updates.$set.undeleted = !changes.deleted;
if (changes.deleted) {
addFlags.push('\\Deleted');
} else {
removeFlags.push('\\Deleted');
}
update = true;
break;
case 'flagged':
updates.$set.flagged = changes.flagged;
if (changes.flagged) {
addFlags.push('\\Flagged');
} else {
removeFlags.push('\\Flagged');
}
update = true;
break;
case 'draft':
updates.$set.flagged = changes.draft;
if (changes.draft) {
addFlags.push('\\Draft');
} else {
removeFlags.push('\\Draft');
}
update = true;
break;
case 'expires':
if (changes.expires) {
updates.$set.exp = true;
updates.$set.rdate = changes.expires.getTime();
} else {
updates.$set.exp = false;
}
update = true;
break;
2018-12-04 16:50:27 +08:00
case 'metaData':
updates.$set['meta.custom'] = changes.metaData;
update = true;
break;
}
});
if (!update) {
return callback(new Error('Nothing was changed'));
}
if (addFlags.length) {
if (!updates.$addToSet) {
updates.$addToSet = {};
}
updates.$addToSet.flags = { $each: addFlags };
}
if (removeFlags.length) {
if (!updates.$pull) {
updates.$pull = {};
}
updates.$pull.flags = { $in: removeFlags };
}
// acquire new MODSEQ
2017-11-22 22:22:36 +08:00
this.database.collection('mailboxes').findOneAndUpdate(
{
_id: mailbox,
user
},
{
$inc: {
// allocate new MODSEQ value
modifyIndex: 1
}
},
{
returnOriginal: false
},
(err, item) => {
if (err) {
return callback(err);
}
2017-11-22 22:22:36 +08:00
if (!item || !item.value) {
return callback(new Error('Mailbox is missing'));
}
2017-11-22 22:22:36 +08:00
let mailboxData = item.value;
2017-11-22 22:22:36 +08:00
updates.$set.modseq = mailboxData.modifyIndex;
2017-11-22 22:22:36 +08:00
let updatedCount = 0;
let cursor = this.database
.collection('messages')
.find({
mailbox: mailboxData._id,
uid: messageQuery
})
.project({
_id: true,
uid: true
});
2017-11-22 22:22:36 +08:00
let done = err => {
let next = () => {
if (err) {
return callback(err);
}
return callback(null, updatedCount);
};
2017-11-22 22:22:36 +08:00
if (notifyEntries.length) {
return this.notifier.addEntries(mailboxData, notifyEntries, () => {
2017-11-22 22:22:36 +08:00
notifyEntries = [];
this.notifier.fire(mailboxData.user);
2017-11-22 22:22:36 +08:00
next();
});
}
2017-11-22 22:22:36 +08:00
next();
};
2017-11-22 22:22:36 +08:00
let processNext = () => {
cursor.next((err, messageData) => {
if (err) {
2017-11-22 22:22:36 +08:00
return done(err);
}
2017-11-22 22:22:36 +08:00
if (!messageData) {
return cursor.close(done);
}
2017-11-22 22:22:36 +08:00
this.database.collection('messages').findOneAndUpdate(
{
_id: messageData._id,
// hash key
mailbox,
uid: messageData.uid
},
updates,
{
projection: {
_id: true,
uid: true,
flags: true
},
returnOriginal: false
},
(err, item) => {
if (err) {
return cursor.close(() => done(err));
}
2017-11-22 22:22:36 +08:00
if (!item || !item.value) {
return processNext();
}
2017-11-22 22:22:36 +08:00
let messageData = item.value;
updatedCount++;
notifyEntries.push({
command: 'FETCH',
uid: messageData.uid,
flags: messageData.flags,
message: messageData._id,
unseenChange: 'seen' in changes
});
if (notifyEntries.length >= consts.BULK_BATCH_SIZE) {
return this.notifier.addEntries(mailboxData, notifyEntries, () => {
2017-11-22 22:22:36 +08:00
notifyEntries = [];
this.notifier.fire(mailboxData.user);
2017-11-22 22:22:36 +08:00
processNext();
});
}
processNext();
2017-11-22 22:22:36 +08:00
}
);
});
2017-11-22 22:22:36 +08:00
};
2017-11-22 22:22:36 +08:00
processNext();
}
);
}
encryptMessage(pubKey, raw, callback) {
if (!pubKey) {
return callback(null, false);
}
if (raw && Array.isArray(raw.chunks) && raw.chunklen) {
raw = Buffer.concat(raw.chunks, raw.chunklen);
}
let lastBytes = [];
let headerEnd = raw.length;
let headerLength = 0;
// split the message into header and body
for (let i = 0, len = raw.length; i < len; i++) {
lastBytes.unshift(raw[i]);
if (lastBytes.length > 10) {
lastBytes.length = 4;
}
if (lastBytes.length < 2) {
continue;
}
let pos = 0;
if (lastBytes[pos] !== 0x0a) {
continue;
}
pos++;
if (lastBytes[pos] === 0x0d) {
pos++;
}
if (lastBytes[pos] !== 0x0a) {
continue;
}
pos++;
if (lastBytes[pos] === 0x0d) {
pos++;
}
// we have a match!'
headerEnd = i + 1 - pos;
headerLength = pos;
break;
}
let header = raw.slice(0, headerEnd);
2018-05-11 19:39:23 +08:00
let breaker = headerLength ? raw.slice(headerEnd, headerEnd + headerLength) : Buffer.alloc(0);
let body = headerEnd + headerLength < raw.length ? raw.slice(headerEnd + headerLength) : Buffer.alloc(0);
// modify headers
let headers = [];
let bodyHeaders = [];
let lastHeader = false;
let boundary = 'nm_' + crypto.randomBytes(14).toString('hex');
let headerLines = header.toString('binary').split('\r\n');
// use for, so we could escape from it if needed
for (let i = 0, len = headerLines.length; i < len; i++) {
let line = headerLines[i];
if (!i || !lastHeader || !/^\s/.test(line)) {
lastHeader = [line];
if (/^content-type:/i.test(line)) {
let parts = line.split(':');
let value = parts.slice(1).join(':');
2020-05-08 15:43:59 +08:00
if (value.split(';').shift().trim().toLowerCase() === 'multipart/encrypted') {
// message is already encrypted, do nothing
return callback(null, false);
}
bodyHeaders.push(lastHeader);
} else if (/^content-transfer-encoding:/i.test(line)) {
bodyHeaders.push(lastHeader);
} else {
headers.push(lastHeader);
}
} else {
lastHeader.push(line);
}
}
headers.push(['Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";'], [' boundary="' + boundary + '"']);
headers.push(['Content-Description: OpenPGP encrypted message']);
headers.push(['Content-Transfer-Encoding: 7bit']);
headers = Buffer.from(headers.map(line => line.join('\r\n')).join('\r\n'), 'binary');
bodyHeaders = Buffer.from(bodyHeaders.map(line => line.join('\r\n')).join('\r\n'), 'binary');
2018-08-15 15:17:02 +08:00
openpgp.key
.readArmored(pubKey)
.then(armored => {
let publicKeys = armored.keys;
openpgp
.encrypt({
message: openpgp.message.fromBinary(Buffer.concat([Buffer.from(bodyHeaders + '\r\n\r\n'), body])),
publicKeys
})
.then(ciphertext => {
let text =
'This is an OpenPGP/MIME encrypted message\r\n\r\n' +
'--' +
boundary +
'\r\n' +
'Content-Type: application/pgp-encrypted\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'Version: 1\r\n' +
'\r\n' +
'--' +
boundary +
'\r\n' +
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
ciphertext.data +
'\r\n--' +
boundary +
'--\r\n';
callback(null, Buffer.concat([headers, breaker, Buffer.from(text)]));
})
.catch(err => {
if (err) {
// ignore
}
// encryption failed, keep message as is
callback(null, false);
});
})
.catch(err => {
if (err) {
// ignore
}
callback(null, false);
});
}
}
module.exports = MessageHandler;