UNPKG

imap-simple

Version:

Wrapper over node-imap, providing a simpler api for common use cases

716 lines (602 loc) 22.8 kB
'use strict'; var Imap = require('imap'); var nodeify = require('nodeify'); var getMessage = require('./helpers/getMessage'); var errors = require('./errors'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var qp = require('quoted-printable'); var iconvlite = require('iconv-lite'); var utf8 = require('utf8'); var uuencode = require('uuencode'); /** * Constructs an instance of ImapSimple * * @param {object} imap a constructed node-imap connection * @constructor * @class ImapSimple */ function ImapSimple(imap) { var self = this; self.imap = imap; // flag to determine whether we should suppress ECONNRESET from bubbling up to listener self.ending = false; // pass most node-imap `Connection` events through 1:1 ['alert', 'mail', 'expunge', 'uidvalidity', 'update', 'close', 'end'].forEach(function (event) { self.imap.on(event, self.emit.bind(self, event)); }); // special handling for `error` event self.imap.on('error', function (err) { // if .end() has been called and an 'ECONNRESET' error is received, don't bubble if (err && self.ending && (err.code.toUpperCase() === 'ECONNRESET')) { return; } self.emit('error', err); }); } util.inherits(ImapSimple, EventEmitter); /** * disconnect from the imap server */ ImapSimple.prototype.end = function () { var self = this; // set state flag to suppress 'ECONNRESET' errors that are triggered when .end() is called. // it is a known issue that has no known fix. This just temporarily ignores that error. // https://github.com/mscdex/node-imap/issues/391 // https://github.com/mscdex/node-imap/issues/395 self.ending = true; // using 'close' event to unbind ECONNRESET error handler, because the node-imap // maintainer claims it is the more reliable event between 'end' and 'close'. // https://github.com/mscdex/node-imap/issues/394 self.imap.once('close', function () { self.ending = false; }); self.imap.end(); }; /** * Open a mailbox * * @param {string} boxName The name of the box to open * @param {function} [callback] Optional callback, receiving signature (err, boxName) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` * @memberof ImapSimple */ ImapSimple.prototype.openBox = function (boxName, callback) { var self = this; if (callback) { return nodeify(this.openBox(boxName), callback); } return new Promise(function (resolve, reject) { self.imap.openBox(boxName, function (err, result) { if (err) { reject(err); return; } resolve(result); }); }); }; /** * Close a mailbox * * @param {boolean} [autoExpunge=true] If autoExpunge is true, any messages marked as Deleted in the currently open mailbox will be remove * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` * @memberof ImapSimple */ ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) { var self = this; if (typeof(autoExpunge) == 'function'){ callback = autoExpunge; autoExpunge = true; } if (callback) { return nodeify(this.closeBox(autoExpunge), callback); } return new Promise(function (resolve, reject) { self.imap.closeBox(autoExpunge, function (err, result) { if (err) { reject(err); return; } resolve(result); }); }); }; /** * Search the currently open mailbox, and retrieve the results * * Results are in the form: * * [{ * attributes: object, * parts: [ { which: string, size: number, body: string }, ... ] * }, ...] * * See node-imap's ImapMessage signature for information about `attributes`, `which`, `size`, and `body`. * For any message part that is a `HEADER`, the body is automatically parsed into an object. * * @param {object} searchCriteria Criteria to use to search. Passed to node-imap's .search() 1:1 * @param {object} fetchOptions Criteria to use to fetch the search results. Passed to node-imap's .fetch() 1:1 * @param {function} [callback] Optional callback, receiving signature (err, results) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `results` * @memberof ImapSimple */ ImapSimple.prototype.search = function (searchCriteria, fetchOptions, callback) { var self = this; if (!callback && typeof fetchOptions === 'function') { callback = fetchOptions; fetchOptions = null; } if (callback) { return nodeify(this.search(searchCriteria, fetchOptions), callback); } return new Promise(function (resolve, reject) { self.imap.search(searchCriteria, function (err, uids) { if (err) { reject(err); return; } if (!uids.length) { resolve([]); return; } var fetch = self.imap.fetch(uids, fetchOptions); var messagesRetrieved = 0; var messages = []; function fetchOnMessage(message, seqNo) { getMessage(message).then(function (message) { message.seqNo = seqNo; messages[seqNo] = message; messagesRetrieved++; if (messagesRetrieved === uids.length) { fetchCompleted(); } }); } function fetchCompleted() { // pare array down while keeping messages in order var pared = messages.filter(function (m) { return !!m; }); resolve(pared); } function fetchOnError(err) { fetch.removeListener('message', fetchOnMessage); fetch.removeListener('end', fetchOnEnd); reject(err); } function fetchOnEnd() { fetch.removeListener('message', fetchOnMessage); fetch.removeListener('error', fetchOnError); } fetch.on('message', fetchOnMessage); fetch.once('error', fetchOnError); fetch.once('end', fetchOnEnd); }); }); }; /** * Download a "part" (either a portion of the message body, or an attachment) * * @param {object} message The message returned from `search()` * @param {object} part The message part to be downloaded, from the `message.attributes.struct` Array * @param {function} [callback] Optional callback, receiving signature (err, data) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `data` * @memberof ImapSimple */ ImapSimple.prototype.getPartData = function (message, part, callback) { var self = this; if (callback) { return nodeify(self.getPartData(message, part), callback); } return new Promise(function (resolve, reject) { var fetch = self.imap.fetch(message.attributes.uid, { bodies: [part.partID], struct: true }); function fetchOnMessage(msg) { getMessage(msg).then(function (result) { if (result.parts.length !== 1) { reject(new Error('Got ' + result.parts.length + ' parts, should get 1')); return; } var data = result.parts[0].body; var encoding = part.encoding.toUpperCase(); if (encoding === 'BASE64') { resolve(new Buffer(data, 'base64')); return; } if (encoding === 'QUOTED-PRINTABLE') { if (part.params && part.params.charset && part.params.charset.toUpperCase() === 'UTF-8') { resolve((new Buffer(utf8.decode(qp.decode(data)))).toString()); } else { resolve((new Buffer(qp.decode(data))).toString()); } return; } if (encoding === '7BIT') { resolve((new Buffer(data)).toString('ascii')); return; } if (encoding === '8BIT' || encoding === 'BINARY') { var charset = (part.params && part.params.charset) || 'utf-8'; resolve(iconvlite.decode(new Buffer(data), charset)); return; } if (encoding === 'UUENCODE') { var parts = data.toString().split('\n'); // remove newline characters var merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string resolve(uuencode.decode(merged)); return; } // if it gets here, the encoding is not currently supported reject(new Error('Unknown encoding ' + part.encoding)); }); } function fetchOnError(err) { fetch.removeListener('message', fetchOnMessage); fetch.removeListener('end', fetchOnEnd); reject(err); } function fetchOnEnd() { fetch.removeListener('message', fetchOnMessage); fetch.removeListener('error', fetchOnError); } fetch.once('message', fetchOnMessage); fetch.once('error', fetchOnError); fetch.once('end', fetchOnEnd); }); }; /** * Moves the specified message(s) in the currently open mailbox to another mailbox. * * @param {string|Array} source The node-imap `MessageSource` indicating the message(s) from the current open mailbox * to move. * @param {string} boxName The mailbox to move the message(s) to. * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.moveMessage = function (source, boxName, callback) { var self = this; if (callback) { return nodeify(self.moveMessage(source, boxName), callback); } return new Promise(function (resolve, reject) { self.imap.move(source, boxName, function (err) { if (err) { reject(err); return; } resolve(); }); }); }; /** * Adds the provided label(s) to the specified message(s). * * This is a Gmail extension method (X-GM-EXT-1) * * @param {string|Array} source The node-imap `MessageSource` indicating the message(s) to add the label(s) to. * @param {string|Array} labels Either a single string or an array of strings indicating the labels to add to the * message(s). * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.addMessageLabel = function (source, labels, callback) { var self = this; if (callback) { return nodeify(self.addMessageLabel(source, labels), callback); } return new Promise(function (resolve, reject) { self.imap.addLabels(source, labels, function (err) { if (err) { reject(err); return; } resolve(); }); }); }; /** * Remove the provided label(s) from the specified message(s). * * This is a Gmail extension method (X-GM-EXT-1) * * @param {string|Array} source The node-imap `MessageSource` indicating the message(s) to remove the label(s) from. * @param {string|Array} labels Either a single string or an array of strings indicating the labels to remove from the * message(s). * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.removeMessageLabel = function (source, labels, callback) { var self = this; if (callback) { return nodeify(self.removeMessageLabel(source, labels), callback); } return new Promise(function (resolve, reject) { self.imap.delLabels(source, labels, function (err) { if (err) { reject(err); return; } resolve(); }); }); }; /** * Adds the provided flag(s) to the specified message(s). * * @param {string|Array} uid The messages uid * @param {string|Array} flags Either a single string or an array of strings indicating the flags to add to the * message(s). * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.addFlags = function (uid, flags, callback) { var self = this; if (callback) { return nodeify(self.addFlags(uid, flags), callback); } return new Promise(function (resolve, reject) { self.imap.addFlags(uid, flags, function (err) { if (err) { reject(err); return; } resolve(); }); }); }; /** * Removes the provided flag(s) to the specified message(s). * * @param {string|Array} uid The messages uid * @param {string|Array} flags Either a single string or an array of strings indicating the flags to remove from the * message(s). * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.delFlags = function (uid, flags, callback) { var self = this; if (callback) { return nodeify(self.delFlags(uid, flags), callback); } return new Promise(function (resolve, reject) { self.imap.delFlags(uid, flags, function (err) { if (err) { reject(err); return; } resolve(); }); }); }; /** * Deletes the specified message(s). * * @param {string|Array} uid The uid or array of uids indicating the messages to be deleted * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.deleteMessage = function (uid, callback) { var self = this; if (callback) { return nodeify(self.deleteMessage(uid), callback); } return new Promise(function (resolve, reject) { self.imap.addFlags(uid, '\\Deleted', function (err) { if (err) { reject(err); return; } self.imap.expunge( function (err) { if (err) { reject(err); return; } resolve(); }); }); }); }; /** * Appends a mime-encoded message to a mailbox * * @param {string|Buffer} message The messages to append to the mailbox * @param {object} [options] * @param {string} [options.mailbox] The mailbox to append the message to. Defaults to the currently open mailbox. * @param {string|Array<String>} [options.flag] A single flag (e.g. 'Seen') or an array of flags (e.g. ['Seen', 'Flagged']) to append to the message. Defaults to no flags. * @param {function} [callback] Optional callback, receiving signature (err) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @memberof ImapSimple */ ImapSimple.prototype.append = function (message, options, callback) { var self = this; if (callback) { return nodeify(self.append(message, options), callback); } return new Promise(function (resolve, reject) { self.imap.append(message, options, function (err) { if (err) { reject(err); return; } resolve(); }); }); }; /** * Returns a list of mailboxes (folders). * * @param {function} [callback] Optional callback containing 'boxes' object. * @returns {undefined|Promise} Returns a promise when no callback is specified, * resolving when the action succeeds. */ ImapSimple.prototype.getBoxes = function (callback) { var self = this; if (callback) { return nodeify(self.getBoxes(), callback); } return new Promise(function (resolve, reject) { self.imap.getBoxes(function (err, boxes) { if (err) { reject(err); return; } resolve(boxes); }); }); }; /** * Add new mailbox (folder) * * @param {string} boxName The name of the box to added * @param {function} [callback] Optional callback, receiving signature (err, boxName) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` * @memberof ImapSimple */ ImapSimple.prototype.addBox = function (boxName, callback) { var self = this; if (callback) { return nodeify(this.addBox(boxName), callback); } return new Promise(function (resolve, reject) { self.imap.addBox(boxName, function (err) { if (err) { reject(err); return; } resolve(boxName); }); }); }; /** * Delete mailbox (folder) * * @param {string} boxName The name of the box to deleted * @param {function} [callback] Optional callback, receiving signature (err, boxName) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` * @memberof ImapSimple */ ImapSimple.prototype.delBox = function (boxName, callback) { var self = this; if (callback) { return nodeify(this.delBox(boxName), callback); } return new Promise(function (resolve, reject) { self.imap.delBox(boxName, function (err) { if (err) { reject(err); return; } resolve(boxName); }); }); }; /** * Connect to an Imap server, returning an ImapSimple instance, which is a wrapper over node-imap to * simplify it's api for common use cases. * * @param {object} options * @param {object} options.imap Options to pass to node-imap constructor 1:1 * @param {function} [callback] Optional callback, receiving signature (err, connection) * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `connection` */ function connect(options, callback) { options = options || {}; options.imap = options.imap || {}; // support old connectTimeout config option. Remove in v2.0.0 if (options.hasOwnProperty('connectTimeout')) { console.warn('[imap-simple] connect: options.connectTimeout is deprecated. ' + 'Please use options.imap.authTimeout instead.'); options.imap.authTimeout = options.connectTimeout; } // set default authTimeout options.imap.authTimeout = options.imap.hasOwnProperty('authTimeout') ? options.imap.authTimeout : 2000; if (callback) { return nodeify(connect(options), callback); } return new Promise(function (resolve, reject) { var imap = new Imap(options.imap); function imapOnReady() { imap.removeListener('error', imapOnError); imap.removeListener('close', imapOnClose); imap.removeListener('end', imapOnEnd); resolve(new ImapSimple(imap)); } function imapOnError(err) { if (err.source === 'timeout-auth') { err = new errors.ConnectionTimeoutError(options.imap.authTimeout); } imap.removeListener('ready', imapOnReady); imap.removeListener('close', imapOnClose); imap.removeListener('end', imapOnEnd); reject(err); } function imapOnEnd() { imap.removeListener('ready', imapOnReady); imap.removeListener('error', imapOnError); imap.removeListener('close', imapOnClose); reject(new Error('Connection ended unexpectedly')); } function imapOnClose() { imap.removeListener('ready', imapOnReady); imap.removeListener('error', imapOnError); imap.removeListener('end', imapOnEnd); reject(new Error('Connection closed unexpectedly')); } imap.once('ready', imapOnReady); imap.once('error', imapOnError); imap.once('close', imapOnClose); imap.once('end', imapOnEnd); if (options.hasOwnProperty('onmail')) { imap.on('mail', options.onmail); } if (options.hasOwnProperty('onexpunge')) { imap.on('expunge', options.onexpunge); } if (options.hasOwnProperty('onupdate')) { imap.on('update', options.onupdate); } imap.connect(); }); } /** * Given the `message.attributes.struct`, retrieve a flattened array of `parts` objects that describe the structure of * the different parts of the message's body. Useful for getting a simple list to iterate for the purposes of, * for example, finding all attachments. * * Code taken from http://stackoverflow.com/questions/25247207/how-to-read-and-save-attachments-using-node-imap * * @param {Array} struct The `message.attributes.struct` value from the message you wish to retrieve parts for. * @param {Array} [parts] The list of parts to push to. * @returns {Array} a flattened array of `parts` objects that describe the structure of the different parts of the * message's body */ function getParts(struct, parts) { parts = parts || []; for (var i = 0; i < struct.length; i++) { if (Array.isArray(struct[i])) { getParts(struct[i], parts); } else if (struct[i].partID) { parts.push(struct[i]); } } return parts; } module.exports = { connect: connect, ImapSimple: ImapSimple, parseHeader: Imap.parseHeader, getParts: getParts, errors: errors };