wildduck/imap.js

640 lines
21 KiB
JavaScript
Raw Normal View History

2017-03-06 22:13:40 +08:00
'use strict';
const log = require('npmlog');
2017-07-16 19:37:33 +08:00
const config = require('wild-config');
2017-03-06 22:13:40 +08:00
const IMAPServerModule = require('./imap-core');
const IMAPServer = IMAPServerModule.IMAPServer;
const ImapNotifier = require('./lib/imap-notifier');
2017-03-06 22:13:40 +08:00
const Indexer = require('./imap-core/lib/indexer/indexer');
const MessageHandler = require('./lib/message-handler');
2017-04-21 01:10:03 +08:00
const UserHandler = require('./lib/user-handler');
2017-07-21 02:33:41 +08:00
const MailboxHandler = require('./lib/mailbox-handler');
const db = require('./lib/db');
const consts = require('./lib/consts');
2017-10-03 16:18:23 +08:00
const RedFour = require('ioredfour');
const packageData = require('./package.json');
const yaml = require('js-yaml');
const fs = require('fs');
const certs = require('./lib/certs');
2017-09-26 14:58:37 +08:00
const setupIndexes = yaml.safeLoad(fs.readFileSync(__dirname + '/indexes.yaml', 'utf8'));
2017-03-06 22:13:40 +08:00
const onFetch = require('./lib/handlers/on-fetch');
const onAuth = require('./lib/handlers/on-auth');
const onList = require('./lib/handlers/on-list');
const onLsub = require('./lib/handlers/on-lsub');
const onSubscribe = require('./lib/handlers/on-subscribe');
const onUnsubscribe = require('./lib/handlers/on-unsubscribe');
const onCreate = require('./lib/handlers/on-create');
const onRename = require('./lib/handlers/on-rename');
const onDelete = require('./lib/handlers/on-delete');
const onOpen = require('./lib/handlers/on-open');
const onStatus = require('./lib/handlers/on-status');
const onAppend = require('./lib/handlers/on-append');
const onStore = require('./lib/handlers/on-store');
const onExpunge = require('./lib/handlers/on-expunge');
const onCopy = require('./lib/handlers/on-copy');
const onMove = require('./lib/handlers/on-move');
const onSearch = require('./lib/handlers/on-search');
const onGetQuotaRoot = require('./lib/handlers/on-get-quota-root');
const onGetQuota = require('./lib/handlers/on-get-quota');
2017-11-07 00:00:09 +08:00
let logger = {
info(...args) {
args.shift();
log.info('IMAP', ...args);
},
2017-11-07 00:00:09 +08:00
debug(...args) {
args.shift();
log.silly('IMAP', ...args);
},
2017-11-07 00:00:09 +08:00
error(...args) {
args.shift();
log.error('IMAP', ...args);
}
};
2017-03-06 22:13:40 +08:00
2017-11-07 00:00:09 +08:00
let indexer;
let notifier;
let messageHandler;
let userHandler;
2017-07-21 02:33:41 +08:00
let mailboxHandler;
let gcTimeout;
let gcLock;
function clearExpiredMessages() {
clearTimeout(gcTimeout);
let startTime = Date.now();
// First, acquire the lock. This prevents multiple connected clients for deleting the same messages
gcLock.acquireLock('gc_expired', Math.round(consts.GC_INTERVAL * 1.2) /* Lock expires if not released */, (err, lock) => {
if (err) {
2017-11-07 00:00:09 +08:00
logger.error(
{
tnx: 'gc',
err
},
'Failed to acquire lock error=%s',
err.message
);
gcTimeout = setTimeout(clearExpiredMessages, consts.GC_INTERVAL);
gcTimeout.unref();
return;
} else if (!lock.success) {
2017-11-17 19:37:53 +08:00
logger.debug(
{
tnx: 'gc'
},
'Lock already acquired'
);
gcTimeout = setTimeout(clearExpiredMessages, consts.GC_INTERVAL);
gcTimeout.unref();
return;
}
2017-11-17 19:37:53 +08:00
logger.debug(
{
tnx: 'gc'
},
'Got lock for garbage collector'
);
let done = () => {
gcLock.releaseLock(lock, err => {
if (err) {
2017-11-07 00:00:09 +08:00
logger.error(
{
tnx: 'gc',
err
},
'Failed to release lock error=%s',
err.message
);
}
gcTimeout = setTimeout(clearExpiredMessages, consts.GC_INTERVAL);
gcTimeout.unref();
});
};
2017-11-17 19:37:53 +08:00
if (config.imap.disableRetention) {
2017-06-06 14:19:35 +08:00
// delete all attachments that do not have any active links to message objects
2017-11-17 19:37:53 +08:00
// do not touch expired messages
return messageHandler.attachmentStorage.deleteOrphaned(() => done(null, true));
2017-06-06 14:19:35 +08:00
}
2017-11-17 19:37:53 +08:00
let deleteOrphaned = next => {
// delete all attachments that do not have any active links to message objects
messageHandler.attachmentStorage.deleteOrphaned(() => {
next(null, true);
});
2017-11-17 19:37:53 +08:00
};
let archiveExpiredMessages = next => {
logger.debug(
{
tnx: 'gc'
},
'Archiving expired messages'
);
// find and delete all messages that are expired
// NB! scattered query, searches over all mailboxes and thus over all shards
let cursor = db.database
.collection('messages')
.find({
exp: true,
rdate: {
$lte: Date.now()
}
})
.project({
_id: true,
mailbox: true,
uid: true,
size: true,
'mimeTree.attachmentMap': true,
magic: true,
unseen: true
});
2017-11-17 19:37:53 +08:00
let deleted = 0;
let clear = () =>
cursor.close(() => {
2017-07-31 15:59:18 +08:00
if (deleted) {
2017-11-07 00:00:09 +08:00
logger.debug(
2017-07-31 15:59:18 +08:00
{
tnx: 'gc'
},
'Deleted %s messages',
deleted
);
}
2017-11-17 19:37:53 +08:00
return deleteOrphaned(next);
});
2017-11-17 19:37:53 +08:00
let processNext = () => {
if (Date.now() - startTime > consts.GC_INTERVAL * 0.8) {
// deleting expired messages has taken too long time, cancel
return clear();
}
2017-11-17 19:37:53 +08:00
cursor.next((err, messageData) => {
if (err) {
return done(err);
}
if (!messageData) {
return clear();
}
2017-11-17 19:37:53 +08:00
messageHandler.del(
{
messageData,
// do not archive messages of deleted users
archive: !messageData.userDeleted
},
err => {
if (err) {
logger.error(
{
tnx: 'gc',
err
},
'Failed to delete expired message id=%s. %s',
messageData._id,
err.message
);
return cursor.close(() => done(err));
}
logger.debug(
{
tnx: 'gc',
err
},
'Deleted expired message id=%s',
messageData._id
);
deleted++;
if (consts.GC_DELAY_DELETE) {
setTimeout(processNext, consts.GC_DELAY_DELETE);
} else {
setImmediate(processNext);
}
}
);
});
};
processNext();
};
let purgeExpiredMessages = next => {
logger.debug(
{
tnx: 'gc'
},
'Purging archived messages'
);
2017-11-17 19:37:53 +08:00
// find and delete all messages that are expired
// NB! scattered query, searches over all mailboxes and thus over all shards
let cursor = db.database
.collection('archived')
.find({
exp: true,
rdate: {
$lte: Date.now()
}
})
.project({
_id: true,
mailbox: true,
uid: true,
size: true,
'mimeTree.attachmentMap': true,
2017-11-22 22:22:36 +08:00
'meta.queueId': true,
2017-11-17 19:37:53 +08:00
magic: true,
unseen: true
});
let deleted = 0;
let clear = () =>
cursor.close(() => {
if (deleted) {
logger.debug(
{
tnx: 'gc'
},
'Purged %s messages',
deleted
);
}
return deleteOrphaned(next);
});
let processNext = () => {
if (Date.now() - startTime > consts.GC_INTERVAL * 0.8) {
// deleting expired messages has taken too long time, cancel
return clear();
}
cursor.next((err, messageData) => {
if (err) {
return done(err);
}
if (!messageData) {
return clear();
}
db.database.collection('archived').deleteOne({ _id: messageData._id }, err => {
if (err) {
2017-11-17 19:37:53 +08:00
//failed to delete
logger.error(
{
tnx: 'gc',
err
},
'Failed to delete archived message id=%s. %s',
messageData._id,
err.message
);
return cursor.close(() => done(err));
}
2017-11-17 19:37:53 +08:00
logger.debug(
{
tnx: 'gc'
},
'Deleted archived message id=%s',
messageData._id
);
let attachmentIds = Object.keys(messageData.mimeTree.attachmentMap || {}).map(key => messageData.mimeTree.attachmentMap[key]);
2017-11-22 22:22:36 +08:00
return db.database.collection('messagelog').insertOne(
{
2017-12-27 05:25:26 +08:00
id: (messageData.meta && messageData.meta.queueId) || messageData._id.toString(),
2017-11-22 22:22:36 +08:00
action: 'DELETED',
parentId: messageData._id,
created: new Date()
},
() => {
if (!attachmentIds.length) {
// no stored attachments
deleted++;
if (consts.GC_DELAY_DELETE) {
setTimeout(processNext, consts.GC_DELAY_DELETE);
} else {
setImmediate(processNext);
}
return;
}
2017-11-17 19:37:53 +08:00
2017-11-22 22:22:36 +08:00
messageHandler.attachmentStorage.updateMany(attachmentIds, -1, -messageData.magic, err => {
if (err) {
// should we care about this error?
}
deleted++;
if (consts.GC_DELAY_DELETE) {
setTimeout(processNext, consts.GC_DELAY_DELETE);
} else {
setImmediate(processNext);
}
});
2017-11-17 19:37:53 +08:00
}
2017-11-22 22:22:36 +08:00
);
2017-11-17 19:37:53 +08:00
});
});
};
processNext();
};
2017-11-17 19:37:53 +08:00
archiveExpiredMessages(() => purgeExpiredMessages(done));
});
}
2017-11-07 00:00:09 +08:00
let createInterface = (ifaceOptions, callback) => {
// Setup server
const serverOptions = {
secure: ifaceOptions.secure,
2017-12-01 16:02:40 +08:00
secured: ifaceOptions.secured,
2017-11-07 00:00:09 +08:00
disableSTARTTLS: ifaceOptions.disableSTARTTLS,
ignoreSTARTTLS: ifaceOptions.ignoreSTARTTLS,
useProxy: !!config.imap.useProxy,
ignoredHosts: config.imap.ignoredHosts,
id: {
name: config.imap.name || 'Wild Duck IMAP Server',
version: config.imap.version || packageData.version,
vendor: config.imap.vendor || 'Kreata'
},
logger,
maxMessage: config.imap.maxMB * 1024 * 1024,
maxStorage: config.maxStorage * 1024 * 1024
};
certs.loadTLSOptions(serverOptions, 'imap');
const server = new IMAPServer(serverOptions);
certs.registerReload(server, 'imap');
let started = false;
server.on('error', err => {
if (!started) {
started = true;
return callback(err);
}
logger.error(
{
err
},
'%s',
err.message
);
});
server.indexer = indexer;
server.notifier = notifier;
// setup command handlers for the server instance
2017-12-08 20:29:00 +08:00
server.onFetch = onFetch(server, messageHandler, userHandler.userCache);
server.onAuth = onAuth(server, userHandler, userHandler.userCache);
2017-11-07 00:00:09 +08:00
server.onList = onList(server);
server.onLsub = onLsub(server);
server.onSubscribe = onSubscribe(server);
server.onUnsubscribe = onUnsubscribe(server);
server.onCreate = onCreate(server, mailboxHandler);
server.onRename = onRename(server, mailboxHandler);
server.onDelete = onDelete(server, mailboxHandler);
server.onOpen = onOpen(server);
server.onStatus = onStatus(server);
2017-12-08 20:29:00 +08:00
server.onAppend = onAppend(server, messageHandler, userHandler.userCache);
2017-11-07 00:00:09 +08:00
server.onStore = onStore(server);
server.onExpunge = onExpunge(server, messageHandler);
server.onCopy = onCopy(server, messageHandler);
server.onMove = onMove(server, messageHandler);
server.onSearch = onSearch(server);
server.onGetQuotaRoot = onGetQuotaRoot(server);
server.onGetQuota = onGetQuota(server);
// start listening
server.listen(ifaceOptions.port, ifaceOptions.host, () => {
if (started) {
return server.close();
}
started = true;
callback(null, server);
});
};
module.exports = done => {
2017-04-13 16:35:39 +08:00
if (!config.imap.enabled) {
return setImmediate(() => done(null, false));
}
2017-07-12 02:38:23 +08:00
gcLock = new RedFour({
2017-10-03 16:18:23 +08:00
redis: db.redis,
2017-07-12 02:38:23 +08:00
namespace: 'wildduck'
});
gcTimeout = setTimeout(clearExpiredMessages, consts.GC_INTERVAL);
2017-07-12 02:38:23 +08:00
gcTimeout.unref();
let start = () => {
2017-11-07 00:00:09 +08:00
indexer = new Indexer({
database: db.database
});
2017-03-06 22:13:40 +08:00
// setup notification system for updates
2017-11-07 00:00:09 +08:00
notifier = new ImapNotifier({
2017-07-17 21:32:31 +08:00
database: db.database,
redis: db.redis
});
2017-08-07 02:25:10 +08:00
messageHandler = new MessageHandler({
database: db.database,
redis: db.redis,
2017-08-07 02:25:10 +08:00
gridfs: db.gridfs,
attachments: config.attachments
2017-08-07 02:25:10 +08:00
});
userHandler = new UserHandler({
database: db.database,
users: db.users,
2017-08-08 19:35:18 +08:00
redis: db.redis,
authlogExpireDays: config.log.authlogExpireDays
2017-08-07 02:25:10 +08:00
});
mailboxHandler = new MailboxHandler({
database: db.database,
users: db.users,
redis: db.redis,
2017-11-07 00:00:09 +08:00
notifier
2017-08-07 02:25:10 +08:00
});
2017-07-21 02:33:41 +08:00
2017-11-07 00:00:09 +08:00
let ifaceOptions = [
{
enabled: true,
secure: config.imap.secure,
disableSTARTTLS: config.imap.disableSTARTTLS,
ignoreSTARTTLS: config.imap.ignoreSTARTTLS,
host: config.imap.host,
port: config.imap.port
}
2017-11-07 00:00:09 +08:00
]
.concat(config.imap.interface || [])
.filter(iface => iface.enabled);
let iPos = 0;
let startInterfaces = () => {
if (iPos >= ifaceOptions.length) {
return done();
2017-03-06 22:13:40 +08:00
}
2017-11-07 00:00:09 +08:00
let opts = ifaceOptions[iPos++];
2017-11-07 00:00:09 +08:00
createInterface(opts, err => {
if (err) {
logger.error(
{
err,
tnx: 'bind'
},
'Failed starting %sIMAP interface %s:%s. %s',
opts.secure ? 'secure ' : '',
opts.host,
opts.port,
err.message
);
return done(err);
}
setImmediate(startInterfaces);
});
};
setImmediate(startInterfaces);
};
2017-09-26 14:58:37 +08:00
let collections = setupIndexes.collections;
let collectionpos = 0;
let ensureCollections = next => {
if (collectionpos >= collections.length) {
2017-11-07 00:00:09 +08:00
logger.info(
2017-09-26 14:58:37 +08:00
{
tnx: 'mongo'
},
'Setup %s collections',
collections.length
);
return next();
}
let collection = collections[collectionpos++];
db[collection.type || 'database'].createCollection(collection.collection, collection.options, err => {
if (err) {
2017-11-07 00:00:09 +08:00
logger.error(
2017-09-26 14:58:37 +08:00
{
err,
tnx: 'mongo'
},
'Failed creating collection %s %s. %s',
collectionpos,
JSON.stringify(collection.collection),
err.message
);
}
ensureCollections(next);
});
};
let indexes = setupIndexes.indexes;
let indexpos = 0;
2017-07-12 02:38:23 +08:00
let ensureIndexes = next => {
2017-09-26 14:58:37 +08:00
if (indexpos >= indexes.length) {
2017-11-07 00:00:09 +08:00
logger.info(
{
tnx: 'mongo'
},
'Setup indexes for %s collections',
2017-09-26 14:58:37 +08:00
indexes.length
);
2017-07-12 02:38:23 +08:00
return next();
}
2017-09-26 14:58:37 +08:00
let index = indexes[indexpos++];
db[index.type || 'database'].collection(index.collection).createIndexes([index.index], (err, r) => {
2017-07-12 02:38:23 +08:00
if (err) {
2017-11-07 00:00:09 +08:00
logger.error(
2017-07-12 02:38:23 +08:00
{
err,
tnx: 'mongo'
},
'Failed creating index %s %s. %s',
2017-07-12 02:38:23 +08:00
indexpos,
2017-07-24 21:44:08 +08:00
JSON.stringify(index.collection + '.' + index.index.name),
2017-07-12 02:38:23 +08:00
err.message
);
} else if (r.numIndexesAfter !== r.numIndexesBefore) {
2017-11-07 00:00:09 +08:00
logger.debug(
2017-07-12 02:38:23 +08:00
{
tnx: 'mongo'
},
'Created index %s %s',
indexpos,
2017-07-24 21:44:08 +08:00
JSON.stringify(index.collection + '.' + index.index.name)
2017-07-12 02:38:23 +08:00
);
} else {
2017-11-07 00:00:09 +08:00
logger.debug(
2017-07-12 02:38:23 +08:00
{
tnx: 'mongo'
},
'Skipped index %s %s: %s',
indexpos,
2017-07-24 21:44:08 +08:00
JSON.stringify(index.collection + '.' + index.index.name),
2017-07-12 02:38:23 +08:00
r.note || 'No index added'
);
}
2017-07-12 02:38:23 +08:00
ensureIndexes(next);
});
};
gcLock.acquireLock('db_indexes', 1 * 60 * 1000, (err, lock) => {
2017-07-12 02:38:23 +08:00
if (err) {
2017-11-07 00:00:09 +08:00
logger.error(
2017-07-12 02:38:23 +08:00
{
tnx: 'gc',
err
},
'Failed to acquire lock error=%s',
err.message
);
return start();
} else if (!lock.success) {
return start();
}
2017-07-12 02:38:23 +08:00
2017-09-26 14:58:37 +08:00
ensureCollections(() => {
ensureIndexes(() => {
// Do not release the indexing lock immediatelly
setTimeout(() => {
gcLock.releaseLock(lock, err => {
if (err) {
2017-11-07 00:00:09 +08:00
logger.error(
2017-09-26 14:58:37 +08:00
{
tnx: 'gc',
err
},
'Failed to release lock error=%s',
err.message
);
}
});
}, 60 * 1000);
return start();
});
2017-07-12 02:38:23 +08:00
});
});
2017-03-06 22:13:40 +08:00
};