Browse Source

first version

Girish Ramakrishnan 5 months ago
commit
093b4c7718
3 changed files with 377 additions and 0 deletions
  1. 27 0
      check-mail.js
  2. 252 0
      imap-probe.js
  3. 98 0
      package-lock.json

+ 27 - 0
check-mail.js

@@ -0,0 +1,27 @@
+'use strict';
+
+let ImapProbe = require('./imap-probe.js');
+
+function checkEmail(options, callback) {
+    var imap = new ImapProbe({
+        user: process.env.IMAP_USERNAME,
+        password: process.env.IMAP_PASSWORD,
+        host: process.env.IMAP_HOST,
+        port: 993, // imap port
+        tls: true,
+        readOnly: true
+    });
+
+    imap.probe(options, callback);
+}
+
+let resetToken = 'cab0f254f4b93720636cf4b85cda39e17ea9f4547a5299f1dfafef460d2e4605';
+
+let url = 'https://my.userstest.smartserver.io/api/v1/session/account/setup.html?reset_token=' + resetToken + '&email=' + encodeURIComponent('test@cloudron.io');
+
+checkEmail({
+    from: 'no-reply@$userstest.smartserver.io',
+    subject: 'Welcome to Acme Inc',
+    text: url
+});
+

+ 252 - 0
imap-probe.js

@@ -0,0 +1,252 @@
+'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) {
+                if (parts[i] === '') content.push('\r\n');
+                else content.push(quotedPrintable.decode(parts[i]));
+                // maybe we can do : if (!parts[i].endsWith('=')) content.push('\r\n'); // soft line break ends with '='
+            } else {
+                content.push(parts[i]);
+            }
+        }
+    }
+
+    return content.join('');
+}
+
+function ImapProbe(options) {
+    assert(options && typeof options === 'object');
+
+    this._connection = null;
+    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.text && message.body.match(needle.text) === 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 : seq=${message.seqno} from=${message.from[0]} to=${message.to[0]} subject=${message.subject[0]} reason=${JSON.stringify(reason)}`);
+
+    return reason.length === 0;
+}
+
+ImapProbe.prototype._scanBox = function (needle, callback) {
+    var that = this;
+
+    const mailbox = this._options.mailbox || 'INBOX';
+    debug(`scanBox: opening mailbox ${mailbox} as readonly=${!!this._options.readOnly}`);
+
+    this._connection.openBox(mailbox, !!this._options.readOnly, function (error, box) {
+        if (error) return callback(error);
+
+        debug(`mailbox messages total=${box.messages.total} new=${box.messages.new}`);
+
+        let seqs = [];
+        for (let seq = box.messages.total; seq > 0; seq--) seqs.push(seq);
+        let matchedMessage = null;
+
+        // fetch one by one to have consistent seq numbers
+        async.someSeries(seqs, function (seq, iteratorDone) {
+            that._fetchMessage(seq, function (error, message) {
+                debug(`scanBox: fetch message ${seq}`, error);
+                if (error) return iteratorDone(error);
+
+                if (message.attributes.flags.indexOf('\\Deleted') !== -1) return iteratorDone(null, false); // skip deleted
+
+                if (!searchMessage(message, needle)) return iteratorDone(null, false); // continue to next message
+
+                debug('scanBox: found matched message', message);
+                matchedMessage = message;
+                iteratorDone(null, true);
+            });
+        }, function (error, found) {
+            if (error) {
+                debug('scanBox: error', error);
+                return callback(error);
+            }
+
+            debug(`scanBox: scan complete. message=${matchedMessage} found=${found}`);
+
+            if (!found) return callback(new Error('Not found'));
+
+            callback(null, matchedMessage);
+        });
+    });
+};
+
+function regexp(str) {
+    if (!str) return null;
+
+    return new RegExp(str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
+}
+
+// needle { subject, to, from, text }
+ImapProbe.prototype.probe = function (options, callback) {
+    var that = this;
+
+    var times = 10, interval = 30000;
+
+    var needle = {
+        to: regexp(options.to),
+        from: regexp(options.from),
+        subject: regexp(options.subject),
+        text: regexp(options.text)
+    };
+
+    async.retry({ times: times, interval: interval }, function (iteratorCallback) {
+        that._connection = new Imap(that._options);
+        that._connection.once('error', function (error) {
+            debug('Connection error', error);
+            iteratorCallback(error);
+        });
+
+        that._connection.once('end', function() {
+            debug('Connection ended');
+        });
+
+        console.log('probing for ', needle, that._options); // use console because needle has regexp
+
+        that._connection.once('ready', function () {
+            debug('Connection success');
+
+            that._scanBox(needle, function (error, message) {
+                that._connection.end(); // doesn't take a callback !
+                that._connection = null;
+
+                iteratorCallback(error, message);
+            });
+        });
+
+        that._connection.connect();
+    }, function (error, message) {
+        debug('done probing', error, message);
+        callback(error, message);
+    });
+};

+ 98 - 0
package-lock.json

@@ -0,0 +1,98 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
+      "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
+      "requires": {
+        "lodash": "^4.17.11"
+      }
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "imap": {
+      "version": "0.8.19",
+      "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz",
+      "integrity": "sha1-NniHOTSrCc6mukh0HyhNoq9Z2NU=",
+      "requires": {
+        "readable-stream": "1.1.x",
+        "utf7": ">=1.0.2"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+    },
+    "ms": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+      "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+    },
+    "quoted-printable": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/quoted-printable/-/quoted-printable-1.0.1.tgz",
+      "integrity": "sha1-nuv16z0R7vAismT9LStrK7O4TMM=",
+      "requires": {
+        "utf8": "^2.1.0"
+      }
+    },
+    "readable-stream": {
+      "version": "1.1.14",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+      "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.1",
+        "isarray": "0.0.1",
+        "string_decoder": "~0.10.x"
+      }
+    },
+    "semver": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+      "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
+    },
+    "string_decoder": {
+      "version": "0.10.31",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+    },
+    "utf7": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.2.tgz",
+      "integrity": "sha1-lV9JCq5lO6IguUVqCod2wZk2CZE=",
+      "requires": {
+        "semver": "~5.3.0"
+      }
+    },
+    "utf8": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
+      "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY="
+    }
+  }
+}