'use strict'; let IMAPServerModule = require('../index.js'); let IMAPServer = IMAPServerModule.IMAPServer; let MemoryNotifier = IMAPServerModule.MemoryNotifier; let fs = require('fs'); let 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, internaldate: new Date('14-Sep-2013 21:22:28 -0300'), raw: new Buffer('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz') }, { uid: 49, flags: ['\\Seen'], internaldate: new Date(), modseq: 5000, raw: fs.readFileSync(__dirname + '/fixtures/ryan_finnie_mime_torture.eml') }, { uid: 50, flags: ['\\Seen'], modseq: 45, internaldate: new Date(), raw: '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: ;\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: \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' + 'Hello world 3!\r\n' + '------mailcomposer-?=_1-1328088797399--' }, { uid: 52, flags: [], modseq: 4, internaldate: new Date(), raw: 'from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nHello World!' }, { uid: 53, flags: [], modseq: 5, internaldate: new Date() }, { uid: 60, flags: [], modseq: 6, internaldate: 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(), 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.onUpdate = 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 = folder.messages.filter(message => { let match = session.matchSearchQuery(message, options.query); if (match && highestModseq < message.modseq) { highestModseq = message.modseq; } return match; }).map(message => message.uid); callback(null, { uidList, highestModseq }); }; return server; };