diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..34c551e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[*.{js,json}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index e920c16..13a5930 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,11 @@ node_modules # Optional REPL history .node_repl_history + +# Sublime Text +*.sublime-project +*.sublime-workspace + +# Mac OS X +.DS_Store + diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..cf80815 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,18 @@ +{ + "node": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "forin": true, + "immed": true, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "nonew": true, + "quotmark": "single", + "undef": true, + "unused": true, + "trailing": true, + "laxcomma": true +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aeda03c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +sudo: false +language: node_js +branches: + except: + - gh-pages +node_js: + - '5' + - '5.1' + - '4' + - '4.2' + - '4.1' + - '4.0' + - '0.12' + - '0.11' + - '0.10' + - '0.8' + - '0.6' + - 'iojs' +before_script: + - npm install grunt-cli -g diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..be753cd --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,21 @@ +module.exports = function (grunt) { + require('load-grunt-tasks')(grunt); + + grunt.initConfig({ + jshint: { + files: { + src: [ + 'lib/**/*.js', + 'test/**/*.js', + 'indexjs', + 'Gruntfile.js' + ] + }, + options: { + jshintrc: '.jshintrc' + } + } + }); + + grunt.registerTask('default', ['jshint']); +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..659c114 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/postmark-transport'); \ No newline at end of file diff --git a/lib/postmark-transport.js b/lib/postmark-transport.js new file mode 100644 index 0000000..d7c9613 --- /dev/null +++ b/lib/postmark-transport.js @@ -0,0 +1,63 @@ +'use strict'; + +var extend = require('extend'), + postmark = require('postmark'), + pkg = require('../package.json'), + addressFormatter = require('./utils').addressFormatter, + addressParser = require('./utils').addressParser, + headersParser = require('./utils').headersParser; + +function PostmarkTransport(options) { + this.name = 'Postmark'; + this.version = pkg.version; + + options = options || {}; + + var auth = options.auth || {}; + this.client = new postmark.Client(auth.apiKey); +} + +PostmarkTransport.prototype.send = function (mail, callback) { + var data = mail.data || {}; + + var fromAddr = addressParser(data.from)[0] || {}; + var toAddrs = addressParser(data.to) || []; + var ccAddrs = addressParser(data.cc) || []; + var bccAddrs = addressParser(data.bcc) || []; + var headers = headersParser(data.headers || []); + var postmarkOptions = data.postmarkOptions || {}; + + var message = extend(true, { + 'From': addressFormatter(fromAddr) || '', + 'To': toAddrs.map(addressFormatter).join(','), + 'Cc': ccAddrs.map(addressFormatter).join(','), + 'Bcc': bccAddrs.map(addressFormatter).join(','), + 'Subject': data.subject || '', + 'TextBody': data.text || '', + 'HtmlBody': data.html || '', + 'Headers': headers + }, postmarkOptions); + + this.client.sendEmail(message, function (err, res) { + if (err) { return callback(err); } + + var accepted = []; + var rejected = []; + + if (res.ErrorCode === 0) { + accepted.push(res); + } else { + rejected.push(res); + } + + return callback(null, { + messageId: res.MessageID, + accepted: accepted, + rejected: rejected + }); + }); +}; + +module.exports = function (options) { + return new PostmarkTransport(options); +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..28c278f --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,53 @@ +var util = require('util'), + addressparser = require('addressparser'); + +function addressFormatter(addr) { + if (addr.address && addr.name) { + return util.format('"%s" <%s>', addr.name, addr.address); + } + + return addr.address; +} + +function addressParser(addrs) { + if (!Array.isArray(addrs)) { + addrs = [addrs]; + } + + return addrs + .map(function (addr) { + if (typeof addr === 'object') { + return [addr]; + } + + return addressparser(addr); + }) + .reduce(function (previous, current) { + return previous.concat(current); + }); +} + +function headersParser(headers) { + if (!Array.isArray(headers)) { + headers = Object.keys(headers).map(function (name) { + return { + key: name, + value: headers[name] + }; + }); + } + + return headers + .map(function (header) { + return { + 'Name': header.key, + 'Value': header.value + }; + }); +} + +module.exports = { + addressFormatter: addressFormatter, + addressParser: addressParser, + headersParser: headersParser +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..778ae25 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "nodemailer-postmark-transport", + "version": "1.0.0", + "description": "Postmark transport for Nodemailer", + "main": "index.js", + "scripts": { + "test": "grunt && mocha" + }, + "repository": { + "type": "git", + "url": "git@github.com:killmenot/nodemailer-postmark-transport.git" + }, + "keywords": [ + "nodemailer", + "postmark" + ], + "author": { + "name": "Alexey Kucherenko", + "url": "https://github.com/killmenot" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/killmenot/nodemailer-postmark-transport/issues" + }, + "homepage": "https://github.com/RebelMail/nodemailer-postmark-transport", + "dependencies": { + "addressparser": "^1.0.1", + "extend": "^3.0.0", + "postmark": "^1.2.1" + }, + "devDependencies": { + "chai": "^3.5.0", + "grunt": "^1.0.1", + "grunt-contrib-jshint": "^1.0.0", + "load-grunt-tasks": "^3.5.0", + "mocha": "^2.4.5", + "sinon": "^1.17.3" + } +} diff --git a/test/postmark-transport.test.js b/test/postmark-transport.test.js new file mode 100644 index 0000000..30132ef --- /dev/null +++ b/test/postmark-transport.test.js @@ -0,0 +1,362 @@ +/* globals afterEach, beforeEach, describe, it */ + +'use strict'; + +var sinon = require('sinon'), + expect = require('chai').expect, + postmarkTransport = require('../'), + pkg = require('../package.json'); + +describe('PostmarkTransport', function () { + var sandbox, + transport, + options; + + beforeEach(function () { + options = { + auth: { + apiKey: 'POSTMARK_API_TEST' + } + }; + }); + + it('should expose name and version', function () { + transport = postmarkTransport(options); + expect(transport.name).to.equal('Postmark'); + expect(transport.version).to.equal(pkg.version); + }); + + describe('send()', function () { + var mail, + sendEmailStub; + + beforeEach(function () { + transport = postmarkTransport(options); + sandbox = sinon.sandbox.create(); + + mail = { + data: {} + }; + + sendEmailStub = sandbox.stub(transport.client, 'sendEmail').yields(null, {}); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('defaults', function (done) { + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message).to.eql({ + From: '', + To: '', + Cc: '', + Bcc: '', + Subject: '', + TextBody: '', + HtmlBody: '', + Headers: [] + }); + done(); + }); + }); + + describe('to', function () { + it('should parse plain email address', function (done) { + mail.data.to = 'foo@example.org'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.To).to.equal('foo@example.org'); + done(); + }); + }); + + it('should parse email address with formatted name', function (done) { + mail.data.to = '"John Doe" '; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.To).to.equal('"John Doe" '); + done(); + }); + }); + + it('should parse address object', function (done) { + mail.data.to = { + name: 'Jane Doe', + address: 'jane.doe@example.org' + }; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.To).to.equal('"Jane Doe" '); + done(); + }); + }); + + it('should parse mixed', function (done) { + mail.data.to = [ + 'foo@example.org', + '"Bar Bar" bar@example.org', + '"Jane Doe" , "John, Doe" ', + { + name: 'Baz', + address: 'baz@example.org' + } + ]; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.To).to.equal([ + 'foo@example.org', + '"Bar Bar" ', + '"Jane Doe" ', + '"John, Doe" ', + '"Baz" ', + ].join(',')); + done(); + }); + }); + }); + + describe('from', function () { + it('should parse plain email address', function (done) { + mail.data.from = 'foo@example.org'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.From).to.equal('foo@example.org'); + done(); + }); + }); + + it('should parse email address with formatted name', function (done) { + mail.data.from = '"John Doe" '; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.From).to.equal('"John Doe" '); + done(); + }); + }); + + it('should parse address object', function (done) { + mail.data.from = { + name: 'Jane Doe', + address: 'jane.doe@example.org' + }; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.From).to.equal('"Jane Doe" '); + done(); + }); + }); + + it('should parse mixed', function (done) { + mail.data.from = '"Jane Doe" , "John, Doe" '; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.From).to.equal('"Jane Doe" '); + done(); + }); + }); + }); + + describe('cc', function () { + it('should parse plain email address', function (done) { + mail.data.cc = 'foo@example.org'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Cc).to.equal('foo@example.org'); + done(); + }); + }); + + it('should parse email address with formatted name', function (done) { + mail.data.cc = '"John Doe" '; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Cc).to.equal('"John Doe" '); + done(); + }); + }); + + it('should parse address object', function (done) { + mail.data.cc = { + name: 'Jane Doe', + address: 'jane.doe@example.org' + }; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Cc).to.equal('"Jane Doe" '); + done(); + }); + }); + + it('should parse mixed', function (done) { + mail.data.cc = [ + 'foo@example.org', + '"Bar Bar" bar@example.org', + '"Jane Doe" , "John, Doe" ', + { + name: 'Baz', + address: 'baz@example.org' + } + ]; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Cc).to.equal([ + 'foo@example.org', + '"Bar Bar" ', + '"Jane Doe" ', + '"John, Doe" ', + '"Baz" ', + ].join(',')); + done(); + }); + }); + }); + + describe('bcc', function () { + it('should parse plain email address', function (done) { + mail.data.bcc = 'foo@example.org'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Bcc).to.equal('foo@example.org'); + done(); + }); + }); + + it('should parse email address with formatted name', function (done) { + mail.data.bcc = '"John Doe" '; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Bcc).to.equal('"John Doe" '); + done(); + }); + }); + + it('should parse address object', function (done) { + mail.data.bcc = { + name: 'Jane Doe', + address: 'jane.doe@example.org' + }; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Bcc).to.equal('"Jane Doe" '); + done(); + }); + }); + + it('should parse mixed', function (done) { + mail.data.bcc = [ + 'foo@example.org', + '"Bar Bar" bar@example.org', + '"Jane Doe" , "John, Doe" ', + { + name: 'Baz', + address: 'baz@example.org' + } + ]; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Bcc).to.equal([ + 'foo@example.org', + '"Bar Bar" ', + '"Jane Doe" ', + '"John, Doe" ', + '"Baz" ', + ].join(',')); + done(); + }); + }); + }); + + describe('subject', function () { + it('should be passed', function (done) { + mail.data.subject = 'Subject'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Subject).to.equal('Subject'); + done(); + }); + }); + }); + + describe('text', function () { + it('should be passed', function (done) { + mail.data.text = 'Hello'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.TextBody).to.equal('Hello'); + done(); + }); + }); + }); + + describe('html', function () { + it('should be passed', function (done) { + mail.data.html = '

Hello

'; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.HtmlBody).to.equal('

Hello

'); + done(); + }); + }); + }); + + describe('headers', function () { + it('should parse {"X-Key-Name": "key value"}', function (done) { + mail.data.headers = { + 'X-Key-Name': 'key value' + }; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Headers).to.eql([ + { + Name: 'X-Key-Name', + Value: 'key value' + } + ]); + done(); + }); + }); + + it('should parse [{key: "X-Key-Name", value: "key value"}]', function (done) { + mail.data.headers = [ + { + key: 'X-Key-Name', + value: 'key value' + } + ]; + + transport.send(mail, function () { + var message = sendEmailStub.args[0][0]; + expect(message.Headers).to.eql([ + { + Name: 'X-Key-Name', + Value: 'key value' + } + ]); + done(); + }); + }); + }); + }); +}); diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..e9a9c73 --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,20 @@ +/* globals describe */ + +'use strict'; + +// var expect = require('chai').expect, +// utils = require('../lib/utils'); + +describe('utils', function () { + describe('addressFormatter()', function () { + + }); + + describe('addressParser()', function () { + + }); + + describe('headersParser()', function () { + + }); +});