221 lines
7.0 KiB
JavaScript
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();
|
|
};
|