cloudron-e2e-test/imap-probe.js

221 lines
7.0 KiB
JavaScript

'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();
};