wildduck/lib/handlers/on-copy.js

281 lines
8.3 KiB
JavaScript
Raw Normal View History

'use strict';
const ObjectId = require('mongodb').ObjectId;
const db = require('../db');
2017-07-17 03:40:34 +08:00
const tools = require('../tools');
const consts = require('../consts');
async function copyHandler(server, messageHandler, connection, mailbox, update, session) {
const socket = (session.socket && session.socket._parent) || session.socket;
server.logger.debug(
{
tnx: 'copy',
cid: session.id
},
'[%s] Copying messages from "%s" to "%s"',
session.id,
mailbox,
update.destination
);
tools.checkSocket(socket);
2017-08-07 02:25:10 +08:00
let userData = await db.users.collection('users').findOne(
{
2020-09-08 15:00:17 +08:00
_id: session.user.id
},
2019-01-24 05:38:29 +08:00
{
2020-09-08 15:00:17 +08:00
maxTimeMS: consts.DB_MAX_TIME_USERS
}
);
if (!userData) {
throw new Error('User not found');
}
if (userData.quota && userData.storageUsed > userData.quota) {
return 'OVERQUOTA';
}
let mailboxData = await db.database.collection('mailboxes').findOne(
{
_id: mailbox
2019-01-24 05:38:29 +08:00
},
{
maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
}
);
2020-09-08 15:00:17 +08:00
if (!mailboxData) {
return 'NONEXISTENT';
}
let targetData = await db.database.collection('mailboxes').findOne(
{
user: session.user.id,
path: update.destination
},
{
maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
}
);
if (!targetData) {
return 'TRYCREATE';
}
let cursor = await db.database
.collection('messages')
.find({
mailbox: mailboxData._id,
uid: tools.checkRangeQuery(update.messages)
}) // no projection as we need to copy the entire message
.sort({ uid: 1 })
.maxTimeMS(consts.DB_MAX_TIME_MESSAGES);
let copiedMessages = 0;
let copiedStorage = 0;
let updateQuota = async () => {
if (!copiedMessages) {
return;
}
try {
let r = await db.users.collection('users').findOneAndUpdate(
{
_id: mailboxData.user
},
2019-01-24 05:38:29 +08:00
{
$inc: {
storageUsed: copiedStorage
}
2019-01-24 05:38:29 +08:00
},
{
2021-06-15 15:47:18 +08:00
returnDocument: 'after',
projection: {
storageUsed: true
},
maxTimeMS: consts.DB_MAX_TIME_USERS
}
);
if (r && r.value) {
server.loggelf({
short_message: '[QUOTA] +',
_mail_action: 'quota',
_user: mailboxData.user,
_inc: copiedStorage,
_copied_messages: copiedMessages,
_storage_used: r.value.storageUsed,
_mailbox: targetData._id,
_sess: session && session.id
});
}
} catch (err) {
// ignore
}
};
let sourceUid = [];
let destinationUid = [];
let messageData;
// COPY might take a long time to finish, so send unsolicited responses
let notifyTimeout;
let notifyLongRunning = () => {
clearTimeout(notifyTimeout);
notifyTimeout = setTimeout(() => {
connection.send('* OK Still processing...');
notifyLongRunning();
}, consts.LONG_COMMAND_NOTIFY_TTL);
};
notifyLongRunning();
try {
while ((messageData = await cursor.next())) {
tools.checkSocket(socket); // do we even have to copy anything?
// this query points to current message
let existingQuery = {
mailbox: messageData.mailbox,
uid: messageData.uid,
_id: messageData._id
};
// Copying is not done in bulk to minimize risk of going out of sync with incremental UIDs
sourceUid.unshift(messageData.uid);
let item = await db.database.collection('mailboxes').findOneAndUpdate(
{
_id: targetData._id
},
{
$inc: {
uidNext: 1
}
},
{
projection: {
uidNext: true,
modifyIndex: true
},
2021-06-15 15:47:18 +08:00
returnDocument: 'before',
maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
}
);
if (!item || !item.value) {
// mailbox not found
return 'TRYCREATE';
}
let uidNext = item.value.uidNext;
let modifyIndex = item.value.modifyIndex;
destinationUid.unshift(uidNext);
messageData._id = new ObjectId();
messageData.mailbox = targetData._id;
messageData.uid = uidNext;
// retention settings
messageData.exp = !!targetData.retention;
messageData.rdate = Date.now() + (targetData.retention || 0);
messageData.modseq = modifyIndex; // reset message modseq to whatever it is for the mailbox right now
2020-09-17 17:42:41 +08:00
if (!messageData.flags.includes('\\Deleted')) {
2020-09-17 17:36:20 +08:00
messageData.searchable = true;
} else {
delete messageData.searchable;
}
let junk = false;
if (targetData.specialUse === '\\Junk' && !messageData.junk) {
messageData.junk = true;
junk = 1;
} else if (targetData.specialUse !== '\\Trash' && messageData.junk) {
delete messageData.junk;
junk = -1;
}
if (!messageData.meta) {
messageData.meta = {};
}
if (!messageData.meta.events) {
messageData.meta.events = [];
}
messageData.meta.events.push({
action: 'IMAPCOPY',
time: new Date()
});
await db.database.collection('messages').updateOne(
existingQuery,
{
$set: {
// indicate that we do not need to archive this message when deleted
copied: true
}
},
2021-02-26 20:00:13 +08:00
{ writeConcern: 'majority' }
);
2021-02-26 20:00:13 +08:00
let r = await db.database.collection('messages').insertOne(messageData, { writeConcern: 'majority' });
if (!r || !r.acknowledged) {
continue;
}
copiedMessages++;
copiedStorage += Number(messageData.size) || 0;
let attachmentIds = Object.keys(messageData.mimeTree.attachmentMap || {}).map(key => messageData.mimeTree.attachmentMap[key]);
if (attachmentIds.length) {
try {
await messageHandler.attachmentStorage.updateMany(attachmentIds, 1, messageData.magic);
} catch (err) {
// should we care about this error?
}
}
let entry = {
command: 'EXISTS',
uid: messageData.uid,
message: messageData._id,
2021-08-07 06:28:21 +08:00
unseen: messageData.unseen,
2022-12-11 21:09:24 +08:00
idate: messageData.idate,
thread: messageData.thread
};
if (junk) {
entry.junk = junk;
}
await new Promise(resolve => server.notifier.addEntries(targetData, entry, resolve));
}
} finally {
clearTimeout(notifyTimeout);
try {
await cursor.close();
} catch (err) {
//ignore, might be already closed
}
await updateQuota();
}
server.notifier.fire(session.user.id, targetData.path);
return [
true,
{
uidValidity: targetData.uidValidity,
sourceUid,
destinationUid
}
];
}
// COPY / UID COPY sequence mailbox
module.exports = (server, messageHandler) => (connection, mailbox, update, session, callback) => {
copyHandler(server, messageHandler, connection, mailbox, update, session)
.then(args => callback(null, ...[].concat(args || [])))
.catch(err => callback(err));
};