diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c93135..44ba359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,26 @@ # Change Log All notable changes to this project will be documented in this file. -## [0.0.3] - 2015-06-15 +## 2.0.0 - 2016-09-09 + +### Added +- MailHops SSL +- MailHops API v2 +- Test for no IPs in header +- Time Traveled, total time it took for the email to reach you +- Bump version 2.0 to match API v2 + +### Fixed +- Received IP sorting +- Received IP duplicates + +## 0.0.3 - 2015-06-15 ### Added - Header IP parsing -## [0.0.1] - 2015-06-14 +## 0.0.1 - 2015-06-14 ### Added - MailHops API lookup endpoint call -- MailHops API config \ No newline at end of file +- MailHops API config diff --git a/README.md b/README.md index de55653..5630a9c 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,22 @@ [www.MailHops.com](http://www.mailhops.com) [![Build Status](https://travis-ci.org/avantassel/mailhops-node.svg)](https://travis-ci.org/avantassel/mailhops-node) +[![NPM version](https://img.shields.io/npm/v/mailhops.svg)](https://www.npmjs.com/package/mailhops) -MailHops logo +MailHops logo A nodejs module for interacting with the MailHops API. -##Getting Started +## Getting Started -###Installation +### Installation ``` $ npm install mailhops ``` -###Configuration -Simply require the mailhops module, instantiate a new MailHops object, configure it if necessary, and start making calls. +### Configuration +Simply require the mailhops module, instantiate a new MailHops object. New MailHops objects can be instantiated with configuration parameters. Here is an example: @@ -24,15 +25,14 @@ New MailHops objects can be instantiated with configuration parameters. Here is var MailHops = require("mailhops"); var mailhops = new MailHops({ api_key: "aWN8Pb27Xj6GfV8D6ARsjokonYwbWUNbz9rM", - api_version: 1, + api_version: 2, proxy: "http://myproxy:3128", - app_name: "Node App v1.0.0", - forecastio_api_key: "", - show_client: 1 + app_name: "Node App v2.0.0", + forecastio_api_key: "" }); ``` -MailHops objects can also be configured via the ```.configure(options)``` method. Here is an exmaple: +MailHops objects can also be configured via the `.configure(options)` method. Here is an exmaple: ```javascript var MailHops = require("mailhops"); @@ -43,25 +43,27 @@ var options = { } mailhops.configure(options); + // get IPs from a full header where headerText is the full header var ips = mailhops.getIPsFromHeader(headerText); // or pass in an array of IP addresses var ips = ['216.58.217.46','98.138.253.109']; -mailhops.lookup(ips,function(err,response){ - console.log(response); +mailhops.lookup(ips,function(err, res, body){ + if(err) + console.log('MailHops Error',err); + console.log(body); }); var mapUrl = mailhops.mapUrl('216.58.217.46,98.138.253.109'); ``` -###Running Tests +### Running Tests ``` $ npm test ``` ## Other MailHops projects -- [API](https://github.com/avantassel/mailhops-api) -- [Postbox & Thunderbird plugin](https://github.com/avantassel/mailhops-plugin) \ No newline at end of file +- https://github.com/mailhops diff --git a/config.json b/config.json index 0c72b87..1f84532 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,7 @@ { - "base_uri": "http://api.mailhops.com" - ,"app_name": "Node App v1.0.0" - ,"api_version": 1 + "base_uri": "https://api.mailhops.com" + ,"app_name": "Node App v2.0.0" + ,"api_version": 2 ,"api_key": "" ,"forecastio_api_key": "" - ,"show_client": 1 -} \ No newline at end of file +} diff --git a/lib/api.js b/lib/api.js index ceffde8..f2ac341 100644 --- a/lib/api.js +++ b/lib/api.js @@ -5,6 +5,8 @@ var request = require([__dirname, "request"].join("/")); module.exports = { + time_traveled: null, + lookup: function(route, options, fn){ if(_.isFunction(options) && _.isUndefined(fn)){ fn = options; @@ -12,12 +14,13 @@ module.exports = { } var qs = options; - qs.api_key = this.api_key || ''; - qs.c = this.show_client; + qs.api_key = qs.api_key || this.api_key || ''; + qs.c = this.show_client || 1; qs.r = Array.isArray(route) ? route.join(',').replace(" ", "") : route.replace(" ", ""); - - if(this.forecastio_api_key) - qs.fkey = this.forecastio_api_key; + if(!!this.forecastio_api_key) + qs.fkey = this.forecastio_api_key; + if(!!this.time_traveled) + qs.t = this.time_traveled; var config = { uri: [this.api_version, "lookup"].join("/"), @@ -31,12 +34,12 @@ module.exports = { //just returns a map url that can be used as an iframe src mapUrl: function(route, options){ var qs = options || {}; - qs.api_key = this.api_key || ''; - qs.c = this.show_client; + qs.api_key = qs.api_key || this.api_key || ''; + qs.c = this.show_client || 1; qs.r = Array.isArray(route) ? route.join(',').replace(" ", "") : route.replace(" ", ""); - if(this.forecastio_api_key) - qs.fkey = this.forecastio_api_key; + if(!!this.forecastio_api_key) + qs.fkey = this.forecastio_api_key; return [this.base_uri, this.api_version, "map", '?'+querystring.stringify(qs)].join("/"); }, @@ -48,17 +51,25 @@ module.exports = { ,regexIp=/(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)(\/(?:[012]\d?|3[012]?|[456789])){0,1}$/ ,regexAllIp = /(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)(\/(?:[012]\d?|3[012]?|[456789])){0,1}/g ,regexIPV6 = /(::|(([a-fA-F0-9]{1,4}):){7}(([a-fA-F0-9]{1,4}))|(:(:([a-fA-F0-9]{1,4})){1,6})|((([a-fA-F0-9]{1,4}):){1,6}:)|((([a-fA-F0-9]{1,4}):)(:([a-fA-F0-9]{1,4})){1,6})|((([a-fA-F0-9]{1,4}):){2}(:([a-fA-F0-9]{1,4})){1,5})|((([a-fA-F0-9]{1,4}):){3}(:([a-fA-F0-9]{1,4})){1,4})|((([a-fA-F0-9]{1,4}):){4}(:([a-fA-F0-9]{1,4})){1,3})|((([a-fA-F0-9]{1,4}):){5}(:([a-fA-F0-9]{1,4})){1,2}))/; - + _.each(receivedHeaders, function(line){ - + //IPV6 check if(line.match(regexIPV6)){ - ips.push( line.match(regexIPV6)[0] ); + ips.unshift( line.match(regexIPV6)[0] ); return; } - + var received_ips = line.match(regexAllIp); + if(!received_ips) + return; + + //get unique IPs for each Received header + received_ips = received_ips.filter(function(item, pos) { + return received_ips.indexOf(item) == pos; + }); + //maybe multiple IPs in one Received: line _.each(received_ips, function(ip){ @@ -69,28 +80,32 @@ module.exports = { // Microsoft SMTP Server id 14.3.195.1; something like this should not be included if(!firstchar.match(/\.|\d|\-/) && !lastchar.match(/\.|\d|\-/) - && ( firstchar != '?' && lastchar != '?' ) + && ( firstchar != '?' && lastchar != '?' ) && lastchar != ';' - && regexIp.test(ip) - && ips.indexOf(ip)===-1){ + && regexIp.test(ip)){ - ips.push( ip ); + ips.unshift( ip ); + + } else if(regexIp.test(ip) + && line.indexOf(ip) !== line.lastIndexOf(ip)){ + //check for duplicate IPs in one line + ips.unshift( ip ); } }); }); - + return ips; }, getReceivedHeaders: function(header){ var receivedHeaders = []; - var rline = ''; + var rline = '',firstDate,lastDate; if ( header ){ var headers = header.split("\n"); _.each(headers,function(line){ - + //if the header line begins with Received, X-Received or X-Originating-IP then push that to our array if(line.indexOf('Received:')===0 || line.indexOf('X-Received:')===0){ if(rline != ''){ @@ -111,6 +126,14 @@ module.exports = { } else { rline += line; receivedHeaders.push(rline); + + // first and last dates are used to calculate time_traveled Traveled + if(rline.indexOf(';')!==-1){ + if(!lastDate) + lastDate = rline.substring(rline.indexOf(';')+1).trim(); + firstDate = rline.substring(rline.indexOf(';')+1).trim(); + } + rline = ''; return; } @@ -119,6 +142,24 @@ module.exports = { if(rline != '') receivedHeaders.push(rline); } + + // parse dates + if(firstDate && firstDate.indexOf('(')!==-1) + firstDate = firstDate.substring(0,firstDate.indexOf('(')).trim(); + if(lastDate && lastDate.indexOf('(')!==-1) + lastDate = lastDate.substring(0,lastDate.indexOf('(')).trim(); + if(firstDate && lastDate){ + try { + firstDate = new Date(firstDate); + lastDate = new Date(lastDate); + this.time_traveled = parseInt(lastDate-firstDate); + } catch(e){ + this.time_traveled = null; + } + } else { + this.time_traveled = null; + } + return receivedHeaders; } -} \ No newline at end of file +} diff --git a/lib/request.js b/lib/request.js index 132a11a..7c570b0 100644 --- a/lib/request.js +++ b/lib/request.js @@ -7,10 +7,11 @@ exports.create = function(config, fn){ uri: [configuration.base_uri, config.uri].join("/"), method: "GET", qs: config.qs || {}, - proxy: config.proxy - } - - request(options, function(err, response, body){ - fn(err, JSON.parse(body)); + timeout: 4000 + }; + if(config.proxy) + options.proxy = config.proxy; + request(options, function(error, response, body){ + fn(error, response, JSON.parse(body)); }); } diff --git a/mailhops.js b/mailhops.js index f4925b8..bed0d25 100644 --- a/mailhops.js +++ b/mailhops.js @@ -6,10 +6,10 @@ function MailHops(config){ } MailHops.prototype.configure = function(config){ - this.base_uri = config.base_uri; + this.base_uri = config.base_uri || 'https://api.mailhops.com'; this.api_key = config.api_key || undefined; this.proxy = config.proxy || undefined; - this.api_version = config.api_version || 1; + this.api_version = config.api_version || 2; this.api_version = ["v", this.api_version].join(""); this.app_name = config.app_name; this.forecastio_api_key = config.forecastio_api_key || undefined; diff --git a/package.json b/package.json index 49e7879..933dfab 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,14 @@ { "name": "mailhops", - "version": "0.0.4", + "version": "2.0.0", "description": "A nodejs module for interacting with the MailHops API.", "main": "main.js", - "dependencies": { - "assert": "^1.1.1", - "async": "^0.6.2", - "lodash": "^2.4.1", - "mocha": "^2.2.4", - "qs": "^3.1.0", - "request": "https://registry.npmjs.org/request/-/request-2.34.0.tgz" - }, "scripts": { "test": "mocha" }, "repository": { "type": "git", - "url": "git://github.com/avantassel/mailhops-node.git" + "url": "git://github.com/mailhops/mailhops-node.git" }, "keywords": [ "mailhops", @@ -30,11 +22,11 @@ "name": "Andrew Van Tassel", "email": "andrew@mailhops.com" }, - "license": "GPLv2", + "license": "MIT", "bugs": { - "url": "https://github.com/avantassel/mailhops-node/issues" + "url": "https://github.com/mailhops/mailhops-node/issues" }, - "homepage": "https://github.com/avantassel/mailhops-node", + "homepage": "https://github.com/mailhops/mailhops-node", "_npmUser": { "name": "avantassel", "email": "andrew@andrewvantassel.com" @@ -44,5 +36,15 @@ "name": "avantassel", "email": "andrew@mailhops.com" } - ] + ], + "dependencies": { + "async": "^2.0.1", + "lodash": "^4.15.0", + "qs": "^6.2.1", + "request": "^2.7.4" + }, + "devDependencies": { + "assert": "^1.4.1", + "mocha": "^3.0.2" + } } diff --git a/test/header-test-no-ips.eml b/test/header-test-no-ips.eml new file mode 100644 index 0000000..6ce20a6 --- /dev/null +++ b/test/header-test-no-ips.eml @@ -0,0 +1,5 @@ +Received: (maildir_expire.pl 32008); Sat, 3 Sep 2016 22:21:29 -0800 +From: Inbox Archiver +Date: Sat, 3 Sep 2016 22:21:29 -0800 +To: x15007238 +Subject: [Inbox archiver: 2 old messages moved to .old-messages/] diff --git a/test/header-test.eml b/test/header-test.eml index 7bfbe46..6049d07 100644 --- a/test/header-test.eml +++ b/test/header-test.eml @@ -2,14 +2,14 @@ Return-Path: ; Mon, 15 Jun 2015 10:38:03 -0700 (PDT) -DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; - d=delivery.klaviyomail.com; - h=content-type:mime-version:from:to:subject; s=smtpapi; + for ; Thu, 3 Sep 2014 16:38:38 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; + d=delivery.klaviyomail.com; + h=content-type:mime-version:from:to:subject; s=smtpapi; bh=lu70JKUbpAjCA9G5JXKZECq8PFc=; b=PNXTO8t5e2Bh/kmKAmOYvgQ+MDGfj 4DAOoP00xozjkYc6KMkyOLFUrfRrZEpBYXQC0m6IXDsyiTuk8SkopTpcc8YYitoa yv4eaW1h9XRG0VYvl3RbBX5QFZsikRmReBO9Yu8gdLizZ8X83SF5TCwDLU99V/OL @@ -18,10 +18,10 @@ Received: by filter0500p1mdw1.sendgrid.net with SMTP id filter0500p1mdw1.14444.5 2015-06-15 17:37:59.069788226 +0000 UTC Received: from queue-worker-166.servers.clovesoftware.com (ec2-54-157-138-253.compute-1.amazonaws.com [54.157.138.253]) by ismtpd-013 (SG) with ESMTP id 14df84c98b9.582.18b0cc - for ; Mon, 15 Jun 2015 17:37:58 +0000 (UTC) + for ; Thu, 4 Sep 2014 17:37:58 +0000 (UTC) Received: from ip-10-157-90-72.ec2.internal (localhost [127.0.0.1]) by queue-worker-166.servers.clovesoftware.com (Postfix) with ESMTP id E2343271B8 - for ; Mon, 15 Jun 2015 17:37:58 +0000 (UTC) + for ; Thu, 4 Sep 2014 17:37:58 +0000 (UTC) Received: from edge01.net.lu.se (130.235.56.196) by uwcas04.uw.lu.se (130.235.59.236) with Microsoft SMTP Server (TLS) id 14.3.195.1; Thu, 4 Sep 2014 11:38:30 +0200 @@ -31,14 +31,14 @@ Received: from muon.isy.liu.se (130.236.48.25) by edge01.net.lu.se Received: from ip227-36.wireless.lu.se (ip227-36.wireless.lu.se [130.235.227.36]) (using TLSv1 with cipher AES128-SHA (128/128 bits)) (No client certificate requested) by muon.isy.liu.se (Postfix) with ESMTPSA id - D4B3D802; Thu, 4 Sep 2014 11:38:28 +0200 (MEST) + D4B3D802; Thu, 4 Sep 2014 1:38:28 +0200 (MEST) Content-Type: multipart/alternative; boundary="===============2767014682564602329==" MIME-Version: 1.0 From: ProgrammableWeb Today To: Andrew Van Tassel Subject: Is It Time to Move On from .NET? -Date: Mon, 15 Jun 2015 17:37:58 GMT +Date: Thu, 4 Sep 2014 1:38:28 GMT X-Kmail-Relay: [gmh6f2.krelaymail.com]:587 Message-Id: <20150615173758.E2343271B8@queue-worker-166.servers.clovesoftware.com> X-SG-EID: z/7dFXGDBkx/fUP8kk8C2xB01JCsbu4AgSgValkgiTccFIBxI9L4Y1qe6fbe+Uf2YVSIXGTcm+Mu+n diff --git a/test/mailhops.js b/test/mailhops.js index 0b0c99d..2c103a5 100644 --- a/test/mailhops.js +++ b/test/mailhops.js @@ -14,6 +14,7 @@ describe("mailhops", function(){ it("required api methods exist", function(){ var required_keys = [ "configure", + "time_traveled", "lookup", "mapUrl", "getIPsFromHeader", @@ -23,21 +24,21 @@ describe("mailhops", function(){ assert.deepEqual(_.keys(mailhops.__proto__), required_keys); }); - it("default config parameters are set correctly", function(){ - assert.equal(mailhops.api_version, "v1"); + it("uses default config parameters", function(){ + assert.equal(mailhops.api_version, "v2"); assert.equal(mailhops.api_key, undefined); }); }); describe("configure()", function(){ - it("sets config parameters correctly", function(){ + it("sets config parameters with configure method", function(){ mailhops.configure({ - api_version: 1, + api_version: 2, api_key: "aWN8Pb27Xj6GfV8D6ARsjokonYwbWUNbz9rM", app_name: "Node App" }); - assert.equal(mailhops.api_version, "v1"); + assert.equal(mailhops.api_version, "v2"); assert.equal(mailhops.api_key, "aWN8Pb27Xj6GfV8D6ARsjokonYwbWUNbz9rM"); assert.equal(mailhops.app_name, "Node App"); }); diff --git a/test/main.js b/test/main.js index 28dc6f0..38a62af 100644 --- a/test/main.js +++ b/test/main.js @@ -3,14 +3,13 @@ var assert = require("assert"); var request = require("request"); var fs = require('fs'); var configuration = require([__dirname, "..", "config"].join("/")); -var pkg = require([__dirname, "..", "package"].join("/")); var MailHops = require([__dirname, "..", "main"].join("/")); var mailhops = new MailHops(configuration); console.log('Using %s', mailhops.base_uri); describe("main", function(){ - + describe("new MailHops()", function(){ it("api_version parameter exists", function(){ assert.ok(_.has(mailhops, "api_version")); @@ -28,26 +27,29 @@ describe("main", function(){ assert.ok(_.has(mailhops, "version")); }); - it("version parameter equals that defined in package.json", function(){ - assert.equal(mailhops.version, pkg.version); - }); - }); describe("lookup endpoint", function(){ it('string route should return a 200 response with private ip', function(done){ - mailhops.lookup('127.0.0.1', function(err, response){ - assert.equal(response.meta['code'],200); - assert.equal(response.response.route[0]['private'],true); + mailhops.lookup('127.0.0.1', function(err, res, body){ + assert.equal(res.statusCode,200); + assert.equal(body.response.route[0]['private'],true); done(); }); }); it('array route should return a 200 response with private ip', function(done){ - mailhops.lookup(['127.0.0.1','216.58.217.46','98.138.253.109'], function(err, response){ - assert.equal(response.meta['code'],200); - assert.equal(response.response.route[0]['private'],true); + mailhops.lookup(['127.0.0.1','216.58.217.46','98.138.253.109'], function(err, res, body){ + assert.equal(res.statusCode,200); + assert.equal(body.response.route[0]['private'],true); + done(); + }); + }); + + it('string route should return a 401 response for invalid api_key', function(done){ + mailhops.lookup('127.0.0.1', {'api_key':'aWN8Pb27Xj6GfV8D6ARsjokonYwbWUNbz9rM'}, function(err, res, body){ + assert.equal(res.statusCode,401); done(); }); }); @@ -57,20 +59,64 @@ describe("main", function(){ describe("map endpoint", function(){ it('should return a 200 response', function(done){ - request(mailhops.mapUrl('127.0.0.1'), function (error, response, body) { - assert.equal(response.statusCode,200); + request(mailhops.mapUrl('127.0.0.1'), function (err, res, body) { + assert.equal(res.statusCode,200); done(); }); }); }); - describe("parse header", function(){ + describe("parse header and lookup", function(){ + //read header form file + var header = fs.readFileSync(__dirname+'/header-test.eml',{ encoding: 'utf8' }); + var ips = mailhops.getIPsFromHeader(header); + it('should return an array of 9 Received IPs', function(done){ + assert.equal(ips.length,9); + done(); + }); - it('should return an array of 8 IP addresses', function(done){ - //read header form file - var header = fs.readFileSync(__dirname+'/header-test.eml',{ encoding: 'utf8' }); - assert.equal(mailhops.getIPsFromHeader(header).length,8); + it('should return a time of 10000 milliseconds', function(done){ + var ips = mailhops.getIPsFromHeader(header); + assert.equal(mailhops.time_traveled,10000); + done(); + }); + + it('should find 9 Received IPs', function(done){ + assert.deepEqual(ips,['130.235.227.36', + '130.235.56.196', + '130.236.48.25', + '130.235.59.236', + '130.235.56.196', + '127.0.0.1', + '54.157.138.253', + '198.21.5.108', + '2607:fb90:50f:5547:0:46:e46a:bd01']); + done(); + }); + + it('should return a 200 response and route of 10 hops', function(done){ + mailhops.lookup(mailhops.getIPsFromHeader(header), function(err, res, body){ + assert.equal(res.statusCode,200); + assert.equal(body.response['route'].length,10); + done(); + }); + }); + + }); + + describe("parse header", function(){ + //read header form file + var header = fs.readFileSync(__dirname+'/header-test-no-ips.eml',{ encoding: 'utf8' }); + var ips = mailhops.getIPsFromHeader(header); + + it('should return an array of 0 IP addresses', function(done){ + assert.equal(ips.length,0); + done(); + }); + + it('should return a time of 0 milliseconds', function(done){ + assert.equal(mailhops.time,null); done(); });