mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-11 10:09:20 +08:00
453 lines
19 KiB
JavaScript
453 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('wild-config');
|
|
const imapTools = require('../../imap-core/lib/imap-tools');
|
|
const db = require('../db');
|
|
const tools = require('../tools');
|
|
const consts = require('../consts');
|
|
const base32 = require('base32.js');
|
|
const crypto = require('crypto');
|
|
|
|
// STORE / UID STORE, updates flags for selected UIDs
|
|
module.exports = server => (mailbox, update, session, callback) => {
|
|
server.logger.debug(
|
|
{
|
|
tnx: 'store',
|
|
cid: session.id
|
|
},
|
|
'[%s] Updating messages in "%s"',
|
|
session.id,
|
|
mailbox
|
|
);
|
|
db.database.collection('mailboxes').findOne(
|
|
{
|
|
_id: mailbox
|
|
},
|
|
(err, mailboxData) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (!mailboxData) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let unseenChange = update.value.includes('\\Seen');
|
|
|
|
let modified = [];
|
|
|
|
let newModseq = false;
|
|
let getModseq = next => {
|
|
if (newModseq) {
|
|
return next(null, newModseq);
|
|
}
|
|
db.database.collection('mailboxes').findOneAndUpdate(
|
|
{ _id: mailboxData._id },
|
|
{
|
|
$inc: {
|
|
modifyIndex: 1
|
|
}
|
|
},
|
|
{
|
|
returnOriginal: false
|
|
},
|
|
(err, item) => {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
newModseq = item && item.value && item.value.modifyIndex;
|
|
|
|
next(null, newModseq);
|
|
}
|
|
);
|
|
};
|
|
|
|
let query = {
|
|
mailbox
|
|
};
|
|
|
|
let queryAll = false;
|
|
if (update.messages.length !== session.selected.uidList.length) {
|
|
// do not use uid selector for 1:*
|
|
query.uid = tools.checkRangeQuery(update.messages);
|
|
} else {
|
|
// 1:*
|
|
queryAll = true;
|
|
// uid is part of the sharding key so we need it somehow represented in the query
|
|
query.uid = {
|
|
$gt: 0,
|
|
$lt: mailboxData.uidNext
|
|
};
|
|
}
|
|
|
|
let condstoreEnabled = !!session.selected.condstoreEnabled;
|
|
|
|
let cursor = db.database
|
|
.collection('messages')
|
|
.find(query)
|
|
.project({
|
|
_id: true,
|
|
uid: true,
|
|
flags: true,
|
|
modseq: true
|
|
})
|
|
.sort([['uid', 1]]);
|
|
|
|
let shouldExpunge = false;
|
|
let updateEntries = [];
|
|
let notifyEntries = [];
|
|
|
|
let done = (...args) => {
|
|
if (updateEntries.length) {
|
|
return db.database.collection('messages').bulkWrite(
|
|
updateEntries,
|
|
{
|
|
ordered: false,
|
|
w: 1
|
|
},
|
|
() => {
|
|
updateEntries = [];
|
|
server.notifier.addEntries(mailboxData, notifyEntries, () => {
|
|
notifyEntries = [];
|
|
server.notifier.fire(session.user.id);
|
|
if (args[0]) {
|
|
// first argument is an error
|
|
return callback(...args);
|
|
} else {
|
|
if (config.imap.autoExpunge && shouldExpunge) {
|
|
// shcedule EXPUNGE command for current folder
|
|
let expungeOptions = {
|
|
// create new temporary session so it would not mix with the active one
|
|
id: 'auto.' + base32.encode(crypto.randomBytes(10)).toLowerCase(),
|
|
user: {
|
|
id: session.user.id,
|
|
username: session.user.username
|
|
}
|
|
};
|
|
setImmediate(() => {
|
|
server.onExpunge(mailbox, { silent: true }, expungeOptions, () => false);
|
|
});
|
|
}
|
|
updateMailboxFlags(mailboxData, update, () => callback(...args));
|
|
}
|
|
});
|
|
}
|
|
);
|
|
}
|
|
server.notifier.fire(session.user.id);
|
|
if (args[0]) {
|
|
// first argument is an error
|
|
return callback(...args);
|
|
} else {
|
|
updateMailboxFlags(mailboxData, update, () => callback(...args));
|
|
}
|
|
};
|
|
|
|
// We have to process all messages one by one instead of just calling an update
|
|
// for all messages as we need to know which messages were exactly modified,
|
|
// otherwise we can't send flag update notifications and modify modseq values
|
|
let processNext = () => {
|
|
cursor.next((err, message) => {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
if (!message) {
|
|
return cursor.close(() => done(null, true, modified));
|
|
}
|
|
if (queryAll && !session.selected.uidList.includes(message.uid)) {
|
|
// skip processing messages that we do not know about yet
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
if (update.unchangedSince && message.modseq > update.unchangedSince) {
|
|
modified.push(message.uid);
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
let flagsupdate = false; // query object for updates
|
|
|
|
let updated = false;
|
|
let existingFlags = message.flags.map(flag => flag.toLowerCase().trim());
|
|
switch (update.action) {
|
|
case 'set':
|
|
// check if update set matches current or is different
|
|
if (
|
|
// if length does not match
|
|
existingFlags.length !== update.value.length ||
|
|
// or a new flag was found
|
|
update.value.filter(flag => !existingFlags.includes(flag.toLowerCase().trim())).length
|
|
) {
|
|
updated = true;
|
|
}
|
|
|
|
message.flags = [].concat(update.value);
|
|
|
|
// set flags
|
|
if (updated) {
|
|
flagsupdate = {
|
|
$set: {
|
|
flags: message.flags,
|
|
unseen: !message.flags.includes('\\Seen'),
|
|
flagged: message.flags.includes('\\Flagged'),
|
|
undeleted: !message.flags.includes('\\Deleted'),
|
|
draft: message.flags.includes('\\Draft')
|
|
}
|
|
};
|
|
|
|
if (message.flags.includes('\\Deleted')) {
|
|
shouldExpunge = true;
|
|
}
|
|
|
|
if (!['\\Junk', '\\Trash'].includes(mailboxData.specialUse) && !message.flags.includes('\\Deleted')) {
|
|
flagsupdate.$set.searchable = true;
|
|
} else {
|
|
flagsupdate.$unset = {
|
|
searchable: ''
|
|
};
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'add': {
|
|
let newFlags = [];
|
|
message.flags = message.flags.concat(
|
|
update.value.filter(flag => {
|
|
if (!existingFlags.includes(flag.toLowerCase().trim())) {
|
|
updated = true;
|
|
newFlags.push(flag);
|
|
return true;
|
|
}
|
|
return false;
|
|
})
|
|
);
|
|
|
|
// add flags
|
|
if (updated) {
|
|
flagsupdate = {
|
|
$addToSet: {
|
|
flags: {
|
|
$each: newFlags
|
|
}
|
|
}
|
|
};
|
|
|
|
if (
|
|
newFlags.includes('\\Seen') ||
|
|
newFlags.includes('\\Flagged') ||
|
|
newFlags.includes('\\Deleted') ||
|
|
newFlags.includes('\\Draft')
|
|
) {
|
|
flagsupdate.$set = {};
|
|
if (newFlags.includes('\\Seen')) {
|
|
flagsupdate.$set = {
|
|
unseen: false
|
|
};
|
|
}
|
|
if (newFlags.includes('\\Flagged')) {
|
|
flagsupdate.$set = {
|
|
flagged: true
|
|
};
|
|
}
|
|
if (newFlags.includes('\\Deleted')) {
|
|
shouldExpunge = true;
|
|
flagsupdate.$set = {
|
|
undeleted: false
|
|
};
|
|
flagsupdate.$unset = {
|
|
searchable: ''
|
|
};
|
|
}
|
|
if (newFlags.includes('\\Draft')) {
|
|
flagsupdate.$set = {
|
|
draft: true
|
|
};
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'remove': {
|
|
// We need to use the case of existing flags when removing
|
|
let oldFlags = [];
|
|
let flagsUpdates = update.value.map(flag => flag.toLowerCase().trim());
|
|
message.flags = message.flags.filter(flag => {
|
|
if (!flagsUpdates.includes(flag.toLowerCase().trim())) {
|
|
return true;
|
|
}
|
|
oldFlags.push(flag);
|
|
updated = true;
|
|
return false;
|
|
});
|
|
|
|
// remove flags
|
|
if (updated) {
|
|
flagsupdate = {
|
|
$pull: {
|
|
flags: {
|
|
$in: oldFlags
|
|
}
|
|
}
|
|
};
|
|
if (
|
|
oldFlags.includes('\\Seen') ||
|
|
oldFlags.includes('\\Flagged') ||
|
|
oldFlags.includes('\\Deleted') ||
|
|
oldFlags.includes('\\Draft')
|
|
) {
|
|
flagsupdate.$set = {};
|
|
if (oldFlags.includes('\\Seen')) {
|
|
flagsupdate.$set = {
|
|
unseen: true
|
|
};
|
|
}
|
|
if (oldFlags.includes('\\Flagged')) {
|
|
flagsupdate.$set = {
|
|
flagged: false
|
|
};
|
|
}
|
|
if (oldFlags.includes('\\Deleted')) {
|
|
flagsupdate.$set = {
|
|
undeleted: true
|
|
};
|
|
if (!['\\Junk', '\\Trash'].includes(mailboxData.specialUse)) {
|
|
flagsupdate.$set.searchable = true;
|
|
}
|
|
}
|
|
if (oldFlags.includes('\\Draft')) {
|
|
flagsupdate.$set = {
|
|
draft: false
|
|
};
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
getModseq((err, modseq) => {
|
|
if (err) {
|
|
return cursor.close(() => done(err));
|
|
}
|
|
|
|
if (!update.silent || condstoreEnabled) {
|
|
// print updated state of the message
|
|
session.writeStream.write(
|
|
session.formatResponse('FETCH', message.uid, {
|
|
uid: update.isUid ? message.uid : false,
|
|
flags: message.flags,
|
|
modseq: condstoreEnabled ? modseq : false
|
|
})
|
|
);
|
|
}
|
|
|
|
if (!flagsupdate.$set) {
|
|
flagsupdate.$set = {};
|
|
}
|
|
flagsupdate.$set.modseq = modseq;
|
|
|
|
updateEntries.push({
|
|
updateOne: {
|
|
filter: {
|
|
_id: message._id,
|
|
// include shard key data as well
|
|
mailbox: mailboxData._id,
|
|
uid: message.uid,
|
|
modseq: {
|
|
$lt: modseq
|
|
}
|
|
},
|
|
update: flagsupdate
|
|
}
|
|
});
|
|
|
|
notifyEntries.push({
|
|
command: 'FETCH',
|
|
ignore: session.id,
|
|
uid: message.uid,
|
|
flags: message.flags,
|
|
message: message._id,
|
|
modseq,
|
|
unseenChange
|
|
});
|
|
|
|
if (updateEntries.length >= consts.BULK_BATCH_SIZE) {
|
|
return db.database.collection('messages').bulkWrite(
|
|
updateEntries,
|
|
{
|
|
ordered: false,
|
|
w: 1
|
|
},
|
|
err => {
|
|
updateEntries = [];
|
|
if (err) {
|
|
return cursor.close(() => done(err));
|
|
}
|
|
|
|
server.notifier.addEntries(mailboxData, notifyEntries, () => {
|
|
notifyEntries = [];
|
|
server.notifier.fire(session.user.id);
|
|
return setImmediate(processNext);
|
|
});
|
|
}
|
|
);
|
|
} else {
|
|
return setImmediate(processNext);
|
|
}
|
|
});
|
|
} else {
|
|
return setImmediate(processNext);
|
|
}
|
|
});
|
|
};
|
|
|
|
return setImmediate(processNext);
|
|
}
|
|
);
|
|
};
|
|
|
|
function updateMailboxFlags(mailbox, update, callback) {
|
|
if (update.action === 'remove') {
|
|
// we didn't add any new flags, so there's nothing to update
|
|
return callback();
|
|
}
|
|
|
|
let mailboxFlags = imapTools.systemFlags.concat(mailbox.flags || []).map(flag => flag.trim().toLowerCase());
|
|
let newFlags = [];
|
|
|
|
// find flags that are not listed with mailbox
|
|
update.value.forEach(flag => {
|
|
// limit mailbox flags by 100
|
|
if (mailboxFlags.length + newFlags.length >= 100) {
|
|
return;
|
|
}
|
|
// if mailbox does not have such flag, then add it
|
|
if (!mailboxFlags.includes(flag.toLowerCase().trim())) {
|
|
newFlags.push(flag);
|
|
}
|
|
});
|
|
|
|
// nothing new found
|
|
if (!newFlags.length) {
|
|
return callback();
|
|
}
|
|
|
|
// found some new flags not yet set for mailbox
|
|
// FIXME: Should we send unsolicited FLAGS and PERMANENTFLAGS notifications? Probably not
|
|
return db.database.collection('mailboxes').findOneAndUpdate(
|
|
{
|
|
_id: mailbox._id
|
|
},
|
|
{
|
|
$addToSet: {
|
|
flags: {
|
|
$each: newFlags
|
|
}
|
|
}
|
|
},
|
|
{},
|
|
callback
|
|
);
|
|
}
|