mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-15 05:44:36 +08:00
618 lines
21 KiB
JavaScript
618 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
const IMAPServerModule = require('../index.js');
|
|
const IMAPServer = IMAPServerModule.IMAPServer;
|
|
const MemoryNotifier = require('./memory-notifier.js');
|
|
const fs = require('fs');
|
|
const parseMimeTree = require('../lib/indexer/parse-mime-tree');
|
|
const imapHandler = require('../lib/handler/imap-handler');
|
|
|
|
module.exports = function(options) {
|
|
// This example uses global folders and subscriptions
|
|
let folders = new Map();
|
|
let subscriptions = new WeakSet();
|
|
|
|
[
|
|
{
|
|
path: 'INBOX',
|
|
uidValidity: 123,
|
|
uidNext: 70,
|
|
modifyIndex: 5000,
|
|
messages: [
|
|
{
|
|
uid: 45,
|
|
flags: [],
|
|
modseq: 100,
|
|
idate: new Date('14-Sep-2013 21:22:28 -0300'),
|
|
mimeTree: parseMimeTree(new Buffer('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz\r\n'))
|
|
},
|
|
{
|
|
uid: 49,
|
|
flags: ['\\Seen'],
|
|
idate: new Date(),
|
|
modseq: 5000,
|
|
mimeTree: parseMimeTree(fs.readFileSync(__dirname + '/fixtures/ryan_finnie_mime_torture.eml'))
|
|
},
|
|
{
|
|
uid: 50,
|
|
flags: ['\\Seen'],
|
|
modseq: 45,
|
|
idate: new Date(),
|
|
mimeTree: parseMimeTree(
|
|
'MIME-Version: 1.0\r\n' +
|
|
'From: andris@kreata.ee\r\n' +
|
|
'To: andris@tr.ee\r\n' +
|
|
'Content-Type: multipart/mixed;\r\n' +
|
|
' boundary=\'----mailcomposer-?=_1-1328088797399\'\r\n' +
|
|
'Message-Id: <testmessage-for-bug>;\r\n' +
|
|
'\r\n' +
|
|
'------mailcomposer-?=_1-1328088797399\r\n' +
|
|
'Content-Type: message/rfc822\r\n' +
|
|
'Content-Transfer-Encoding: 7bit\r\n' +
|
|
'\r\n' +
|
|
'MIME-Version: 1.0\r\n' +
|
|
'From: andris@kreata.ee\r\n' +
|
|
'To: andris@pangalink.net\r\n' +
|
|
'In-Reply-To: <test1>\r\n' +
|
|
'\r\n' +
|
|
'Hello world 1!\r\n' +
|
|
'------mailcomposer-?=_1-1328088797399\r\n' +
|
|
'Content-Type: message/rfc822\r\n' +
|
|
'Content-Transfer-Encoding: 7bit\r\n' +
|
|
'\r\n' +
|
|
'MIME-Version: 1.0\r\n' +
|
|
'From: andris@kreata.ee\r\n' +
|
|
'To: andris@pangalink.net\r\n' +
|
|
'\r\n' +
|
|
'Hello world 2!\r\n' +
|
|
'------mailcomposer-?=_1-1328088797399\r\n' +
|
|
'Content-Type: text/html; charset=utf-8\r\n' +
|
|
'Content-Transfer-Encoding: quoted-printable\r\n' +
|
|
'\r\n' +
|
|
'<b>Hello world 3!</b>\r\n' +
|
|
'------mailcomposer-?=_1-1328088797399--\r\n'
|
|
)
|
|
},
|
|
{
|
|
uid: 52,
|
|
flags: [],
|
|
modseq: 4,
|
|
idate: new Date(),
|
|
mimeTree: parseMimeTree('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nHello World!\r\n')
|
|
},
|
|
{
|
|
uid: 53,
|
|
flags: [],
|
|
modseq: 5,
|
|
idate: new Date()
|
|
},
|
|
{
|
|
uid: 60,
|
|
flags: [],
|
|
modseq: 6,
|
|
idate: new Date()
|
|
}
|
|
],
|
|
journal: []
|
|
},
|
|
{
|
|
path: '[Gmail]/Sent Mail',
|
|
specialUse: '\\Sent',
|
|
uidValidity: 123,
|
|
uidNext: 90,
|
|
modifyIndex: 1,
|
|
messages: [],
|
|
journal: []
|
|
}
|
|
].forEach(folder => {
|
|
folders.set(folder.path, folder);
|
|
subscriptions.add(folder);
|
|
});
|
|
|
|
// Setup server
|
|
let server = new IMAPServer(options);
|
|
server.notifier = new MemoryNotifier({
|
|
logger: {
|
|
info: () => false,
|
|
debug: () => false,
|
|
error: () => false
|
|
},
|
|
folders
|
|
});
|
|
|
|
server.on('error', err => {
|
|
console.log('SERVER ERR\n%s', err.stack); // eslint-disable-line no-console
|
|
});
|
|
|
|
server.onAuth = function(login, session, callback) {
|
|
if (login.username !== 'testuser' || login.password !== 'pass') {
|
|
return callback();
|
|
}
|
|
|
|
callback(null, {
|
|
user: {
|
|
username: login.username
|
|
}
|
|
});
|
|
};
|
|
|
|
// LIST "" "*"
|
|
// Returns all folders, query is informational
|
|
// folders is either an Array or a Map
|
|
server.onList = function(query, session, callback) {
|
|
this.logger.debug('[%s] LIST for "%s"', session.id, query);
|
|
|
|
callback(null, folders);
|
|
};
|
|
|
|
// LSUB "" "*"
|
|
// Returns all subscribed folders, query is informational
|
|
// folders is either an Array or a Map
|
|
server.onLsub = function(query, session, callback) {
|
|
this.logger.debug('[%s] LSUB for "%s"', session.id, query);
|
|
|
|
let subscribed = [];
|
|
folders.forEach(folder => {
|
|
if (subscriptions.has(folder)) {
|
|
subscribed.push(folder);
|
|
}
|
|
});
|
|
|
|
callback(null, subscribed);
|
|
};
|
|
|
|
// SUBSCRIBE "path/to/mailbox"
|
|
server.onSubscribe = function(mailbox, session, callback) {
|
|
this.logger.debug('[%s] SUBSCRIBE to "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
subscriptions.add(folders.get(mailbox));
|
|
callback(null, true);
|
|
};
|
|
|
|
// UNSUBSCRIBE "path/to/mailbox"
|
|
server.onUnsubscribe = function(mailbox, session, callback) {
|
|
this.logger.debug('[%s] UNSUBSCRIBE from "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
subscriptions.delete(folders.get(mailbox));
|
|
callback(null, true);
|
|
};
|
|
|
|
// CREATE "path/to/mailbox"
|
|
server.onCreate = function(mailbox, session, callback) {
|
|
this.logger.debug('[%s] CREATE "%s"', session.id, mailbox);
|
|
|
|
if (folders.has(mailbox)) {
|
|
return callback(null, 'ALREADYEXISTS');
|
|
}
|
|
|
|
folders.set(mailbox, {
|
|
path: mailbox,
|
|
uidValidity: Date.now(),
|
|
uidNext: 1,
|
|
modifyIndex: 0,
|
|
messages: [],
|
|
journal: []
|
|
});
|
|
|
|
subscriptions.add(folders.get(mailbox));
|
|
callback(null, true);
|
|
};
|
|
|
|
// RENAME "path/to/mailbox" "new/path"
|
|
// NB! RENAME affects child and hierarchy mailboxes as well, this example does not do this
|
|
server.onRename = function(mailbox, newname, session, callback) {
|
|
this.logger.debug('[%s] RENAME "%s" to "%s"', session.id, mailbox, newname);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
if (folders.has(newname)) {
|
|
return callback(null, 'ALREADYEXISTS');
|
|
}
|
|
|
|
let oldMailbox = folders.get(mailbox);
|
|
folders.delete(mailbox);
|
|
|
|
oldMailbox.path = newname;
|
|
folders.set(newname, oldMailbox);
|
|
|
|
callback(null, true);
|
|
};
|
|
|
|
// DELETE "path/to/mailbox"
|
|
server.onDelete = function(mailbox, session, callback) {
|
|
this.logger.debug('[%s] DELETE "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
// keep SPECIAL-USE folders
|
|
if (folders.get(mailbox).specialUse) {
|
|
return callback(null, 'CANNOT');
|
|
}
|
|
|
|
folders.delete(mailbox);
|
|
callback(null, true);
|
|
};
|
|
|
|
// SELECT/EXAMINE
|
|
server.onOpen = function(mailbox, session, callback) {
|
|
this.logger.debug('[%s] Opening "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let folder = folders.get(mailbox);
|
|
|
|
return callback(null, {
|
|
specialUse: folder.specialUse,
|
|
uidValidity: folder.uidValidity,
|
|
uidNext: folder.uidNext,
|
|
modifyIndex: folder.modifyIndex,
|
|
uidList: folder.messages.map(message => message.uid)
|
|
});
|
|
};
|
|
|
|
// STATUS (X Y X)
|
|
server.onStatus = function(mailbox, session, callback) {
|
|
this.logger.debug('[%s] Requested status for "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let folder = folders.get(mailbox);
|
|
|
|
return callback(null, {
|
|
messages: folder.messages.length,
|
|
uidNext: folder.uidNext,
|
|
uidValidity: folder.uidValidity,
|
|
highestModseq: folder.modifyIndex,
|
|
unseen: folder.messages.filter(message => message.flags.indexOf('\\Seen') < 0).length
|
|
});
|
|
};
|
|
|
|
// APPEND mailbox (flags) date message
|
|
server.onAppend = function(mailbox, flags, date, raw, session, callback) {
|
|
this.logger.debug('[%s] Appending message to "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'TRYCREATE');
|
|
}
|
|
|
|
date = (date && new Date(date)) || new Date();
|
|
|
|
let folder = folders.get(mailbox);
|
|
let message = {
|
|
uid: folder.uidNext++,
|
|
modseq: ++folder.modifyIndex,
|
|
date: (date && new Date(date)) || new Date(),
|
|
mimeTree: parseMimeTree(raw),
|
|
flags
|
|
};
|
|
|
|
folder.messages.push(message);
|
|
|
|
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
|
|
this.notifier.addEntries(
|
|
session.user.username,
|
|
mailbox,
|
|
{
|
|
command: 'EXISTS',
|
|
uid: message.uid
|
|
},
|
|
() => {
|
|
this.notifier.fire(session.user.username, mailbox);
|
|
|
|
return callback(null, true, {
|
|
uidValidity: folder.uidValidity,
|
|
uid: message.uid
|
|
});
|
|
}
|
|
);
|
|
};
|
|
|
|
// STORE / UID STORE, updates flags for selected UIDs
|
|
server.onStore = function(mailbox, update, session, callback) {
|
|
this.logger.debug('[%s] Updating messages in "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let condstoreEnabled = !!session.selected.condstoreEnabled;
|
|
|
|
let modified = [];
|
|
let folder = folders.get(mailbox);
|
|
let i = 0;
|
|
|
|
let processMessages = () => {
|
|
if (i >= folder.messages.length) {
|
|
this.notifier.fire(session.user.username, mailbox);
|
|
return callback(null, true, modified);
|
|
}
|
|
|
|
let message = folder.messages[i++];
|
|
let updated = false;
|
|
|
|
if (update.messages.indexOf(message.uid) < 0) {
|
|
return processMessages();
|
|
}
|
|
|
|
if (update.unchangedSince && message.modseq > update.unchangedSince) {
|
|
modified.push(message.uid);
|
|
return processMessages();
|
|
}
|
|
|
|
switch (update.action) {
|
|
case 'set':
|
|
// check if update set matches current or is different
|
|
if (message.flags.length !== update.value.length || update.value.filter(flag => message.flags.indexOf(flag) < 0).length) {
|
|
updated = true;
|
|
}
|
|
// set flags
|
|
message.flags = [].concat(update.value);
|
|
break;
|
|
|
|
case 'add':
|
|
message.flags = message.flags.concat(
|
|
update.value.filter(flag => {
|
|
if (message.flags.indexOf(flag) < 0) {
|
|
updated = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
})
|
|
);
|
|
break;
|
|
|
|
case 'remove':
|
|
message.flags = message.flags.filter(flag => {
|
|
if (update.value.indexOf(flag) < 0) {
|
|
return true;
|
|
}
|
|
updated = true;
|
|
return false;
|
|
});
|
|
break;
|
|
}
|
|
|
|
// notifiy only if something changed
|
|
if (updated) {
|
|
message.modseq = ++folder.modifyIndex;
|
|
|
|
// Only show response if not silent or modseq is required
|
|
if (!update.silent || condstoreEnabled) {
|
|
session.writeStream.write(
|
|
session.formatResponse('FETCH', message.uid, {
|
|
uid: update.isUid ? message.uid : false,
|
|
flags: update.silent ? false : message.flags,
|
|
modseq: condstoreEnabled ? message.modseq : false
|
|
})
|
|
);
|
|
}
|
|
|
|
this.notifier.addEntries(
|
|
session.user.username,
|
|
mailbox,
|
|
{
|
|
command: 'FETCH',
|
|
ignore: session.id,
|
|
uid: message.uid,
|
|
flags: message.flags
|
|
},
|
|
processMessages
|
|
);
|
|
} else {
|
|
processMessages();
|
|
}
|
|
};
|
|
|
|
processMessages();
|
|
};
|
|
|
|
// EXPUNGE deletes all messages in selected mailbox marked with \Delete
|
|
server.onExpunge = function(mailbox, update, session, callback) {
|
|
this.logger.debug('[%s] Deleting messages from "%s"', session.id, mailbox);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let folder = folders.get(mailbox);
|
|
let deleted = [];
|
|
let i, len;
|
|
|
|
for (i = folder.messages.length - 1; i >= 0; i--) {
|
|
if (
|
|
((update.isUid && update.messages.indexOf(folder.messages[i].uid) >= 0) || !update.isUid) &&
|
|
folder.messages[i].flags.indexOf('\\Deleted') >= 0
|
|
) {
|
|
deleted.unshift(folder.messages[i].uid);
|
|
folder.messages.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
let entries = [];
|
|
for ((i = 0), (len = deleted.length); i < len; i++) {
|
|
entries.push({
|
|
command: 'EXPUNGE',
|
|
ignore: session.id,
|
|
uid: deleted[i]
|
|
});
|
|
if (!update.silent) {
|
|
session.writeStream.write(session.formatResponse('EXPUNGE', deleted[i]));
|
|
}
|
|
}
|
|
|
|
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
|
this.notifier.fire(session.user.username, mailbox);
|
|
return callback(null, true);
|
|
});
|
|
};
|
|
|
|
// COPY / UID COPY sequence mailbox
|
|
server.onCopy = function(mailbox, update, session, callback) {
|
|
this.logger.debug('[%s] Copying messages from "%s" to "%s"', session.id, mailbox, update.destination);
|
|
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
if (!folders.has(update.destination)) {
|
|
return callback(null, 'TRYCREATE');
|
|
}
|
|
|
|
let sourceFolder = folders.get(mailbox);
|
|
let destinationFolder = folders.get(update.destination);
|
|
|
|
let messages = [];
|
|
let sourceUid = [];
|
|
let destinationUid = [];
|
|
let i, len;
|
|
let entries = [];
|
|
|
|
for (i = sourceFolder.messages.length - 1; i >= 0; i--) {
|
|
if (update.messages.indexOf(sourceFolder.messages[i].uid) >= 0) {
|
|
messages.unshift(JSON.parse(JSON.stringify(sourceFolder.messages[i])));
|
|
sourceUid.unshift(sourceFolder.messages[i].uid);
|
|
}
|
|
}
|
|
|
|
for ((i = 0), (len = messages.length); i < len; i++) {
|
|
messages[i].uid = destinationFolder.uidNext++;
|
|
destinationUid.push(messages[i].uid);
|
|
destinationFolder.messages.push(messages[i]);
|
|
|
|
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
|
|
entries.push({
|
|
command: 'EXISTS',
|
|
uid: messages[i].uid
|
|
});
|
|
}
|
|
|
|
this.notifier.addEntries(update.destination, session.user.username, entries, () => {
|
|
this.notifier.fire(update.destination, session.user.username);
|
|
|
|
return callback(null, true, {
|
|
uidValidity: destinationFolder.uidValidity,
|
|
sourceUid,
|
|
destinationUid
|
|
});
|
|
});
|
|
};
|
|
|
|
// sends results to socket
|
|
server.onFetch = function(mailbox, options, session, callback) {
|
|
this.logger.debug('[%s] Requested FETCH for "%s"', session.id, mailbox);
|
|
this.logger.debug('[%s] FETCH: %s', session.id, JSON.stringify(options.query));
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let folder = folders.get(mailbox);
|
|
let entries = [];
|
|
|
|
if (options.markAsSeen) {
|
|
// mark all matching messages as seen
|
|
folder.messages.forEach(message => {
|
|
if (options.messages.indexOf(message.uid) < 0) {
|
|
return;
|
|
}
|
|
|
|
// if BODY[] is touched, then add \Seen flag and notify other clients
|
|
if (message.flags.indexOf('\\Seen') < 0) {
|
|
message.flags.unshift('\\Seen');
|
|
entries.push({
|
|
command: 'FETCH',
|
|
ignore: session.id,
|
|
uid: message.uid,
|
|
flags: message.flags
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
|
let pos = 0;
|
|
let processMessage = () => {
|
|
if (pos >= folder.messages.length) {
|
|
// once messages are processed show relevant updates
|
|
this.notifier.fire(session.user.username, mailbox);
|
|
return callback(null, true);
|
|
}
|
|
let message = folder.messages[pos++];
|
|
|
|
if (options.messages.indexOf(message.uid) < 0) {
|
|
return setImmediate(processMessage);
|
|
}
|
|
|
|
if (options.changedSince && message.modseq <= options.changedSince) {
|
|
return setImmediate(processMessage);
|
|
}
|
|
|
|
let stream = imapHandler.compileStream(
|
|
session.formatResponse('FETCH', message.uid, {
|
|
query: options.query,
|
|
values: session.getQueryResponse(options.query, message)
|
|
})
|
|
);
|
|
|
|
// send formatted response to socket
|
|
session.writeStream.write(stream, () => {
|
|
setImmediate(processMessage);
|
|
});
|
|
};
|
|
|
|
setImmediate(processMessage);
|
|
});
|
|
};
|
|
|
|
// returns an array of matching UID values and the highest modseq of matching messages
|
|
server.onSearch = function(mailbox, options, session, callback) {
|
|
if (!folders.has(mailbox)) {
|
|
return callback(null, 'NONEXISTENT');
|
|
}
|
|
|
|
let folder = folders.get(mailbox);
|
|
let highestModseq = 0;
|
|
|
|
let uidList = [];
|
|
let checked = 0;
|
|
let checkNext = () => {
|
|
if (checked >= folder.messages.length) {
|
|
return callback(null, {
|
|
uidList,
|
|
highestModseq
|
|
});
|
|
}
|
|
let message = folder.messages[checked++];
|
|
session.matchSearchQuery(message, options.query, (err, match) => {
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
if (match && highestModseq < message.modseq) {
|
|
highestModseq = message.modseq;
|
|
}
|
|
if (match) {
|
|
uidList.push(message.uid);
|
|
}
|
|
checkNext();
|
|
});
|
|
};
|
|
checkNext();
|
|
};
|
|
|
|
return server;
|
|
};
|