diff --git a/README.md b/README.md new file mode 100644 index 0000000..889e9e9 --- /dev/null +++ b/README.md @@ -0,0 +1,338 @@ +# Postmark API + +This is a full REST API wrapper for Postmark + +To use the module, run `npm install postmarkapi` + +## Using the module + +First you must make a instance of the module, with your Postmark Server token. + +```js +var PostmarkAPI = require('postmarkapi'); +var pmk = new PostmarkAPI('[your server token]'); +``` + +## Sending an email + +You always need to define a `to` for an email, and `subject`. You can send `text` or `html`, or both, but you need to define one or ther other. + +If you don't define a `from`, it will use the address you created the Sender Signature with. + +You can send multiple `cc`s or `bcc`s by passing an array. + +You don't need to pass a callback. + +```js +// simple example +pmk.email({ + to: 'someaddress@somewhere.com', + subject: 'Test Email', + text: 'Hello World' +}); + +// more specific +pmk.email({ + to: 'someaddress@somewhere.com', + from: 'fromaddress@somewhere.com', + fromName: 'John Doe', + cc: ['carbon-copy@somewhere.com', 'carbon-copy-2@somewhere.com'], + bcc: 'blind-carbon-copy@somewhere.com', + reply: 'reply-to@somewhere.com', + tag: 'MyTag', + headers: { + EmailedUsing: 'Node PostmarkAPI Module' + }, + subject: 'Test Email', + text: 'Hello World', + html: 'Hello World' +}, function(err, response) { + // ... +}); +``` +You can also send an email with attachments + +```js +var path = require('path'); + +pmk.email({ + to: 'someaddress@somewhere.com', + from: 'fromaddress@somewhere.com', + subject: 'Test Email', + text: 'Hello World', + html: 'Hello World', + attachments: [ + path.resolve(__dirname, 'cats.gif'), + path.resolve(__dirname, 'notes.txt') + ] +}, function(err, response) { + // ... +}); +``` + +## Bounces + +### Getting a summary of bounces for the server + +```js +pmk.deliverystats(function(err, response) {}); +``` + +### Retrieving bounces + +You can retrieve bounces associated with your server. + +```js +// simple example +pmk.bounces({ + count: 10, + offset: 0 +}, function(err, response) {}); + +// with messageId +pmk.bounces({ + messageId: '[messageIDHere]' +}, function(err, response) {}); + +// more sepcific +pmk.bounces({ + count: 10, + offset: 0, + type: 'HardBounce', + inactive: 0, + emailFilter: 'somewhere.com' +}, function(err, response) {}); +``` + +### Getting a list of tags for bounces on server + +```js +pmk.bounceTags(function(err, response) {}); +``` + +### Getting a single bounce + +```js +pmk.bounce(bouncId, function(err, response) {}); +``` + +### Getting a single bounce's dump + +```js +pmk.bounceDump(bouncId, function(err, response) {}); +``` + +### Activating a deactivated bounce + +Callback optional + +```js +pmk.bounceActivate(bounceId, function(err, response) {}); +``` + +## Outbound messages + +### Retrieving sent messages + +```js +// simple example +pmk.outbound({ + count: 10, + offset: 0 +}, function(err, response) {}); +``` + +```js +// more specific +pmk.outbound({ + count: 10, + offset: 0, + recipient: 'someone@somewhere.com', + fromemail: 'fromemail@somewhere.com', + tag: 'MyTag', + subject: 'Welcome Email' +}, function(err, response) {}); +``` + +### Getting details for a single sent message + +```js +pmk.outboundMessage(messageId, function(err, response) {}); +``` + +### Getting message dump + +```js +pmk.outboundMessageDump(messageId, function(err, response) {}); +``` + +## Inbound messages + +### Retrieving recieved messages + +```js +// simple example +pmk.inbound({ + count: 10, + offset: 0 +}, function(err, response) {}); +``` + +```js +// more specific +pmk.inbound({ + count: 10, + offset: 0, + recipient: 'someone@somewhere.com', + fromemail: 'fromemail@somewhere.com', + tag: 'MyTag', + subject: 'Welcome Email', + mailboxhash: 'mailboxhashvalue' +}, function(err, response) {}); +``` + +### Gettings details for a single recieved message + +```js +pmk.inboundMessage(messageId, function(err, response) {}); +``` + +## Sender Signatures + +### Getting a list of Sender Signatures + +```js +pmk.senders({ + count: 10, + offset: 0 +}, function(err, response) {}); +``` + +### Fetching details for a single sender + +```js +pmk.sender(senderId, function(err, response) {}); +``` + +### Creating a Sender Signature + +The `reply` and callback are optional + +```js +pmk.createSender({ + name: 'Sender Name', + from: 'senderemail@somewhere.com', + reply: 'replyto@somewhere.com' +}, function(err, response) {}); +``` + +### Updating a Sender Signature + +The `reply` and callback are optional + +You cannot update the `from` address + +```js +pmk.updateSender(senderId, { + name: 'Sender Name', + reply: 'replyto@somewhere.com' +}, function(err, response) {}); +``` + +### Resending a Sender Signature confirmation email + +The callback is optional + +```js +pmk.resendSender(senderId, function(err, response) {}); +``` + +### Deleting a Sender Signature + +The callback is optional + +```js +pmk.deleteSender(senderId, function(err, response) {}); +``` + +### Verifying a SPF record + +The callback is optional + +```js +pmk.verifySPF(senderId, function(err, response) {}); +``` + +### Requesting a new DKIM + +The callback is optional + +```js +pmk.requestDKIM(senderId, function(err, response) {}); +``` + +## Servers + +### Getting a list of servers + +```js +pmk.servers({ + count: 10, + offset: 0, + name: 'Production' +}, function(err, response) {}); +``` + +### Getting a single server's details + +```js +pmk.server(serverId, function(err, response) {}); +``` + +### Creating a new server + +```js +// simple example +pmk.createServer({ + name: 'Server Name' +}); + +// more specific +pmk.createServer({ + name: 'Server Name', + color: 'red', + smtp: true, + raw: true, + inboundHook: 'https://...', + bounceHook: 'https://...', + inboundDomain: 'myDomain' +}, function(err, response) {}); +``` + +### Updating a server + +```js +// simple example +pmk.updateServer(serverId, { + name: 'Server Name' +}); + +// more specific +pmk.updateServer(serverId, { + name: 'Server Name', + color: 'red', + smtp: true, + raw: true, + inboundHook: 'https://...', + bounceHook: 'https://...', + inboundDomain: 'myDomain' +}, function(err, response) {}); +``` + +### Deleting a server + +The callback is optional + +```js +pmk.deleteServer(serverId, function(err, response) {}); +``` diff --git a/package.json b/package.json index d2717ca..b804c99 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "node": ">=0.10.20" }, "dependencies" : { - "request": "2.34.0" + "request": "2.34.0", + "mime": "1.2.11", + "async": "0.7.0" }, "repository": "git://github.com/MadisonReed/postmarkapi" } \ No newline at end of file diff --git a/src/pmk.js b/src/pmk.js index 05a9d27..ec82b87 100644 --- a/src/pmk.js +++ b/src/pmk.js @@ -1,5 +1,9 @@ // external dependencies var request = require('request'); +var async = require('async'); +var fs = require('fs'); +var path = require('path'); +var mime = require('mime'); // internal dependencies var PMKError = require('./error.js'); @@ -7,6 +11,7 @@ var PMKError = require('./error.js'); // globals var nonWord = /[^a-z0-9 ]/i; var noOp = function() {}; +var slice = Array.prototype.slice; /** Constructor for the API @@ -18,7 +23,10 @@ function PMK(token) { throw 'You must initialize with a valid postmark api token'; } - this.token = token; + this.get = curry(makeRequest, token, 'GET'); + this.post = curry(makeRequest, token, 'POST'); + this.put = curry(makeRequest, token, 'PUT'); + this.delete = curry(makeRequest, token, 'DELETE'); } /** @@ -32,13 +40,17 @@ function PMK(token) { @param {String|String[]} message.to Email recipient(s) @param {String|String[]} [message.cc] Carbon copy recipient(s) @param {String|String[]} [message.bcc] Blind carbon copy recipient(s) - @param {String|String[]} [message.reply] Reply To email address + @param {String|String[]} [message.reply] Reply-To email address + @param {String} [message.tag] Tag + @param {Object} [message.headers] Custom headers to send in request (key value pairs) + @param {String[]} Attachments (string paths to local files) @param {String} message.subject Subject @param {String} [message.text] Text body @param {String} [message.html] HTML body @param {Function} [callback] Callback function */ PMK.prototype.email = function(message, callback) { + var self = this; var body = {}; var postmarkKey, key; var recipients = 0; @@ -107,35 +119,455 @@ PMK.prototype.email = function(message, callback) { return; } - // rest of the keys + if (message.headers) { + body.Headers = []; + + for (key in message.headers) { + body.Headers.push({ + Name: key, + Value: message.headers[key] + }); + } + } + + if (message.attachments) { + body.Attachments = []; + + async.each(message.attachments, function(filepath, cb) { + if (typeof filepath !== 'string') { + cb(new PMKError('Attachments must be file paths')); + return; + } + + fs.readFile(filepath, function(err, content) { + if (err) { + cb(err); + return; + } + + body.Attachments.push({ + Name: path.basename(filepath), + Content: content.toString('base64'), + ContentType: mime.lookup(filepath) + }); + cb(); + }); + }, doRequest); + } else { + doRequest(); + } + + function doRequest(err) { + if (err) { + callback(err); + return; + } + + // rest of the keys + var pairs = { + reply: 'ReplyTo', + tag: 'Tag', + html: 'HtmlBody', + text: 'TextBody', + subject: 'Subject', + to: 'To' + } + for (key in pairs) { + if (message[key]) { + body[ pairs[key] ] = message[key]; + } + } + + // finally, the request + self.post('email', body, callback); + } +}; + +/** + Returns a summary of inactive emails and bounces by type over the entire history of the server + + @param {Function} callback Callback function +*/ +PMK.prototype.deliverystats = function(callback) { + this.get('deliverystats', callback); +}; + +/** + Retrieves bounces + + @param {Object} options Options for the request + @param {String|Number} [options.count] Count for paging [Required if not passing messageID] + @param {String|Number} [options.offset] Offset for paging [Required if not passing messageID] + @param {String} [options.type] Bounce type + @param {Boolean} [options.inactive] Filter by inactive / active status + @param {String} [options.emailFilter] Filters out emails that don't match this substring + @param {String} [options.messageID] Returns only messages matching the given message id + @param {Function} callback Callback function +*/ +PMK.prototype.bounces = function(options, callback) { + this.get('bounces', options, callback); +}; + +/** + Gets a single bounce + + @param {String} id The bounce id + @param {Function} callback Callback function +*/ +PMK.prototype.bounce = function(id, callback) { + this.get('bounces/' + id, callback); +}; + +/** + Returns a single bounce's dump + + @param {String} id The bounce id + @param {Function} callback Callback function +*/ +PMK.prototype.bounceDump = function(id, callback) { + this.get('bounces/' + id + '/dump', callback); +}; + +/** + Returns a list of tags used for the current server. + + @param {Function} callback Callback function +*/ +PMK.prototype.bounceTags = function(callback) { + this.get('bounces/tags', callback); +}; + +/** + Activates a deactivated bounce + + @param {String} id The bounce id + @param {Function} [callback] Callback function +*/ +PMK.prototype.bounceActivate = function(id, callback) { + callback = callback || noOp; + + this.put('bounces/' + id + '/active', callback); +}; + +/** + Gets sent messages + + @param {Object} options The filtering options + @param {String|Number} options.count Paging count + @param {String|Number} options.offset Paging offset + @param {String} [options.recipient] Who the message was sent to + @param {String} [options.fromemail] Messages with a given 'from' email address + @param {String} [options.tag] Messages with a given tag + @param {String} [options.subject] Messages with a given subject + @param {Function} callback Callback function +*/ +PMK.prototype.outbound = function(options, callback) { + this.get('messages/outbound', options, callback); +}; + +/** + Get details for a single sent message + + @param {String} id The message id for the email + @param {Function} callback Callback function +*/ +PMK.prototype.outboundMessage = function(id, callback) { + this.get('messages/outbound/' + id + '/details', callback); +}; + +/** + Get sent email dump + + @param {String} id The message id for the email + @param {Function} callback Callback function +*/ +PMK.prototype.outboundMessageDump = function(id, callback) { + this.get('messages/outbound/' + id + '/dump', callback); +}; + +/** + Gets recieved messages + + @param {Object} options The filtering options + @param {String|Number} options.count Paging count + @param {String|Number} options.offset Paging offset + @param {String} [options.recipient] Who the message was sent to + @param {String} [options.fromemail] Messages with a given 'from' email address + @param {String} [options.tag] Messages with a given tag + @param {String} [options.subject] Messages with a given subject + @param {String} [options.mailboxhash] Messages with a given mailboxhash + @param {Function} callback Callback function +*/ +PMK.prototype.inbound = function(options, callback) { + this.get('messages/inbound', options, callback); +}; + +/** + Get details for a single recieved message + + @param {String} id The message id for the email + @param {Function} callback Callback function +*/ +PMK.prototype.inboundMessage = function(id, callback) { + this.get('messages/inbound/' + id + '/details', callback); +}; + +/** + Fetches a list of Sender Signatures + + @param {Object} options The paging options + @param {String|Number} options.count Paging count + @param {String|Number} options.offset Paging offset + @param {Function} callback Callback function +*/ +PMK.prototype.senders = function(options, callback) { + this.get('senders', options, callback); +}; + +/** + Fetches a single Sender's details + + @param {String} id The Sender id + @param {Function} callback Callback function +*/ +PMK.prototype.sender = function(id, callback) { + this.get('senders/' + id, callback); +}; + +/** + Creates a new Sender Signature + + @param {Object} options The creation options + @param {String} options.name The name of the sender + @param {String} options.from The from email address + @param {String} [options.reply] The reply-to email address + @param {Function} [callback] Callback function +*/ +PMK.prototype.createSender = function(options, callback) { + callback = callback || noOp; + + var body = { + Name: options.name, + FromEmail: options.from + }; + + if (options.reply) { + body.ReplyToEmail = options.reply; + } + + this.post('senders', body, callback); +}; + +/** + Updates a Sender Signature + + @param {String} id The sender id + @param {Object} options The update options + @param {String} options.name The name of the sender + @param {String} [options.reply] The reply-to email address + @param {Function} [callback] Callback function +*/ +PMK.prototype.updateSender = function(id, options, callback) { + callback = callback || noOp; + + var body = { + Name: options.name + }; + + if (options.reply) { + body.ReplyToEmail = options.reply; + } + + this.put('senders/' + id, body, callback); +}; + +/** + Resends confirmation email for a Sender Signature + + @param {String} id The sender id + @param {Function} [callback] Callback function +*/ +PMK.prototype.resendSender = function(id, callback) { + callback = callback || noOp; + + this.post('senders/' + id + '/resend', callback); +}; + +/** + Deletes a Sender Signature + + @param {String} id The sender id + @param {Function} [callback] Callback function +*/ +PMK.prototype.deleteSender = function(id, callback) { + callback = callback || noOp; + + this.delete('sender/' + id, callback); +}; + +/** + Verifies a SPF record + + @param {String} id The sender id + @param {Function} [callback] Callback function +*/ +PMK.prototype.verifySPF = function(id, callback) { + callback = callback || noOp; + + this.post('senders/' + id + '/verifyspf', callback); +}; + +/** + Requests a new DKIM + + @param {String} id The sender id + @param {Function} [callback] Callback function +*/ +PMK.prototype.requestDKIM = function(id, callback) { + callback = callback || noOp; + + this.post('senders/' + id + '/requestnewdkim', callback); +}; + +/** + Lists servers + + @param {Object} options The listing options + @param {String|Number} options.count Paging count + @param {String|Number} options.offset Paging offset + @param {String} options.name Server name to search by (.e.g 'production') + @param {Function} callback Callback function +*/ +PMK.prototype.servers = function(options, callback) { + this.get('servers', options, callback); +}; + +/** + Gets a single server's details + + @param {String} id The server's id + @param {Function} callback Callback function +*/ +PMK.prototype.server = function(id, callback) { + this.get('servers/' + id, callback); +}; + +/** + Creates a new server + + @param {Object} options The creation options + @param {String} options.name The name of the server + @param {String} [options.color] The color indicator (e.g. 'red') + @param {Boolean} [options.smtp] Indicate if this Server should have SMTP access turned on + @param {Boolean} [options.raw] Indicate if Inbound web hook http post calls should include the original RAW email in the JSON body + @param {String} [options.inboundHook] Url to send http posts to for Inbound message processing + @param {String} [options.bounceHook] Url to send http posts to for any message bounces that occur on this Server + @param {String} [options.inboundDomain] The MX domain used for MX Inbound processing + @param {Function} [callback] Callback function +*/ +PMK.prototype.createServer = function(options, callback) { + callback = callback || noOp; + + var body = { + Name: options.name + }; + var pairs = { - reply: 'ReplyTo', - tag: 'Tag', - html: 'HtmlBody', - text: 'TextBody', - headers: 'Headers', // to do: make this one better - subject: 'Subject', - to: 'To', - attachments: 'Attachments' // to do: make this one better - } - for (key in pairs) { - if (message[key]) { - body[ pairs[key] ] = message[key]; + color: 'Color', + smtp: 'SmtpApiActivated', + raw: 'RawEmailEnabled', + inboundHook: 'InboundHookUrl', + bounceHook: 'BounceHookUrl', + inboundDomain: 'InboundDomain' + }; + + for (var key in pairs) { + if (options[key]) { + body[ pars[key] ] = options[key]; + } + } + + this.post('servers', body, callback); +}; + +/** + Edits an existing server + + @param {String} id The server id + @param {Object} options The listing options + @param {String} options.name The name of the server + @param {String} [options.color] The color indicator (e.g. 'red') + @param {Boolean} [options.smtp] Indicate if this Server should have SMTP access turned on + @param {Boolean} [options.raw] Indicate if Inbound web hook http post calls should include the original RAW email in the JSON body + @param {String} [options.inboundHook] Url to send http posts to for Inbound message processing + @param {String} [options.bounceHook] Url to send http posts to for any message bounces that occur on this Server + @param {String} [options.inboundDomain] The MX domain used for MX Inbound processing + @param {Function} [callback] Callback function +*/ +PMK.prototype.updateServer = function(id, options, callback) { + callback = callback || noOp; + + var body = {}; + + var pairs = { + name: 'Name', + color: 'Color', + smtp: 'SmtpApiActivated', + raw: 'RawEmailEnabled', + inboundHook: 'InboundHookUrl', + bounceHook: 'BounceHookUrl', + inboundDomain: 'InboundDomain' + }; + + for (var key in pairs) { + if (options[key]) { + body[ pars[key] ] = options[key]; } } - // finally, the request - request({ - method: 'POST', - uri: 'https://api.postmarkapp.com/email', + this.put('servers/' + id, body, callback); +}; + +/** + Deletes an existing server + + @param {String} id The server id + @param {Function} [callback] Callback function +*/ +PMK.prototype.deleteServer = function(id, callback) { + callback = callback || noOp; + + this.delete('servers/' + id, callback); +}; + +// method to make requests to the postmark api +// data is optional (becomes qs or body) +function makeRequest(token, method, pathname, data, callback) { + if (arguments.length < 5) { + callback = data; + data = null; + } + + var options = { + method: method, + uri: 'https://api.postmarkapp.com/' + pathname, headers: { charset: 'utf-8', Accept: 'application/json', 'Content-Type': 'application/json', - 'X-Postmark-Server-Token': this.token - }, - body: JSON.stringify(body) - }, function(err, res, body) { + 'X-Postmark-Server-Token': token + } + }; + + if (data) { + if (method.toUpperCase() === 'POST') { + options.body = JSON.stringify(data); + } else { + options.qs = data; + } + } + + request(options, function(err, res, body) { if (err) { callback(err); return; @@ -150,9 +582,29 @@ PMK.prototype.email = function(message, callback) { return; } - // to do: deal with errors in response + if (result.ErrorCode) { + if (result.Message) { + err = result.Message; + delete result.Message; + } else { + err = 'Failed'; + } + callback(new PMKError(err, result)); + return; + } + callback(null, result); }); -}; +} + +// curry utility +function curry(fn) { + var args = slice.call(arguments, 1); + + return function() { + // keeping the 'this' of this function, which will be useful for prototype methods + return fn.apply(this, args.concat(slice.call(arguments))); + }; +} module.exports = PMK;