'use strict'; exports = module.exports = ImapProbe; var assert = require('assert'), async = require('async'), debug = require('debug')('imap-probe'), Imap = require('imap'), quotedPrintable = require('quoted-printable'); // helper function to parse buffer as a multipart function parseMultipart(buffer, boundary) { var parts = buffer.split('\r\n'); var content = []; var found = false; var headers = false; var consume = false; var encodingQuotedPrintable = false; for (var i = 0; i < parts.length; ++i) { if (parts[i].indexOf('--' + boundary) === 0) { // if we get a second boundary but have already found the plain one, stop if (found) break; content = []; headers = true; continue; } // check if we have found the plain/text section if (headers && parts[i].toLowerCase().indexOf('content-type: text/plain') === 0) { found = true; continue; } if (headers && parts[i].toLowerCase().indexOf('content-transfer-encoding: quoted-printable') === 0) { encodingQuotedPrintable = true; continue; } // we found the headers and an empty newline marks the beginning of the body if (headers && parts[i] === '') { headers = false; consume = true; continue; } if (consume) { if (encodingQuotedPrintable) parts[i] = quotedPrintable.decode(parts[i]); content.push(parts[i]); } } return content.join('\n'); } function ImapProbe(options) { assert(options && typeof options === 'object'); this._connection = new Imap(options); this._options = options; } ImapProbe.prototype._fetchMessage = function (seq, callback) { assert.strictEqual(typeof callback, 'function'); var message = { subject: null, body: null, from: null, to: null, multipartBoundry: null, seqno: null }; var f = this._connection.seq.fetch(seq + ':' + seq, { bodies: ['HEADER.FIELDS (TO)', 'HEADER.FIELDS (FROM)', 'HEADER.FIELDS (SUBJECT)', 'HEADER.FIELDS (CONTENT-TYPE)', 'TEXT'], struct: true }); f.on('message', function (msg, seqno) { message.seqno = seqno; msg.on('body', function (stream, info) { var buffer = ''; stream.on('data', function (chunk) { buffer += chunk.toString('utf8'); }); stream.once('end', function () { if (info.which === 'TEXT') { message.body = buffer; } else if (info.which === 'HEADER.FIELDS (SUBJECT)') { message.subject = Imap.parseHeader(buffer).subject; } else if (info.which === 'HEADER.FIELDS (FROM)') { message.from = Imap.parseHeader(buffer).from; } else if (info.which === 'HEADER.FIELDS (TO)') { message.to = Imap.parseHeader(buffer).to; } else if (info.which === 'HEADER.FIELDS (CONTENT-TYPE)') { if (buffer.indexOf('multipart/alternative') !== -1) { // extract boundary and remove any " or ' message.multipartBoundry = buffer.split('boundary=')[1] .replace(/"([^"]+(?="))"/g, '$1') .replace(/'([^']+(?='))'/g, '$1') .replace(/\r\n/g, ''); } } }); }); msg.once('attributes', function (attrs) { message.attributes = attrs; }); msg.once('end', function () { if (message.multipartBoundry) { message.body = parseMultipart(message.body, message.multipartBoundry); } }); }); f.once('error', callback); f.once('end', function () { callback(null, message); }); }; function searchMessage(message, needle) { assert.strictEqual(typeof message, 'object'); assert.strictEqual(typeof needle, 'object'); var reason = [ ]; if (needle.subject && message.subject[0].match(needle.subject) === null) { reason.push('subject does not match'); } if (needle.body && message.body.match(needle.body) === null) { reason.push('body does not match'); } if (needle.to && message.to[0].match(needle.to) === null) { reason.push('to does not match'); } if (needle.from && message.from[0].match(needle.from) === null) { reason.push('from does not match'); } debug('searchMessage : %s %s %s %s (%j)', message.seqno, message.from[0], message.to[0], message.subject[0], reason); return reason.length === 0; } ImapProbe.prototype._scanBox = function (needle, callback) { var that = this; this._connection.openBox(this._options.mailbox || 'INBOX', !!this._options.readOnly, function (error, box) { if (error) return callback(error); debug('mailbox messages', box.messages); // fetch one by one to have consistent seq numbers var matchedMessage = null; async.whilst(function cond() { return box.messages.total > 0; }, function (iteratorCallback) { that._fetchMessage(box.messages.total--, function (error, message) { if (error) return iteratorCallback(error); if (message.attributes.flags.indexOf('\\Deleted') !== -1) return iteratorCallback(); // skip deleted if (!searchMessage(message, needle)) return iteratorCallback(); // continue to next message matchedMessage = message; iteratorCallback(new Error('Found')); }); }, function whilstDone(error) { if (error && error.message !== 'Found') return callback(error); // imap error async.series([ function moveToTash(done) { if (!matchedMessage || that._options.readOnly) return done(); that._connection.seq.move(matchedMessage.seqno, ['Trash'], done); }, that._connection.closeBox.bind(that._connection) ], function () { if (!matchedMessage) return callback(new Error('Not found')); callback(null, matchedMessage); }); }); }); }; ImapProbe.prototype.probe = function (needle, callback) { var that = this; var times = needle.times || 10, interval = needle.interval || 30000; this._connection.once('error', callback); this._connection.once('end', function() { debug('Connection ended'); }); console.log('probing for ', needle); // use console because needle has regexp this._connection.once('ready', function () { debug('Connection success'); async.retry({ times: times, interval: interval }, that._scanBox.bind(that, needle), function (error, message) { that._connection.end(); // doesn't take a callback ! callback(error, message); }); }); this._connection.connect(); };