UNPKG

@octonary/mailgun-js

Version:

Simple Node.js helper module for Mailgun API. Forked to fix IE issues with ` character.

357 lines (303 loc) 9.43 kB
var https = require('https'); var http = require('http'); var proxy = require('proxy-agent'); var qs = require('querystring'); var fs = require('fs'); var Readable = require('stream').Readable; var FormData = require('form-data'); var Attachment = require('./attachment'); var retry = require('async').retry; var promisifyCall = require('promisify-call'); var debug = require('debug')('mailgun-js'); function noop () { }; function isOk(i) { return typeof i !== 'undefined' && i !== null; } function Request(options) { this.host = options.host; this.protocol = options.protocol; this.port = options.port; this.endpoint = options.endpoint; this.auth = options.auth; this.proxy = options.proxy; this.timeout = options.timeout; this.retry = options.retry || 1; } Request.prototype._request = function (method, resource, data, fn) { var self = this; var path = ''.concat(this.endpoint, resource); var params = this.prepareData(data); this.payload = ''; var isMIME = path.indexOf('/messages.mime') >= 0; this.headers = {}; if (method === 'GET' || method === 'DELETE') { this.payload = qs.stringify(params); if (this.payload) path = path.concat('?', this.payload); } else { this.headers['Content-Type'] = isMIME ? 'multipart/form-data' : 'application/x-www-form-urlencoded'; if (params && (params.attachment || params.inline || (isMIME && params.message))) { this.prepareFormData(params); } else { this.payload = qs.stringify(params); var length = this.payload ? Buffer.byteLength(this.payload) : 0; this.headers['Content-Length'] = length; } } // check for MIME is true in case of messages GET if (method === 'GET' && path.indexOf('/messages') >= 0 && params && params.MIME === true) { this.headers.Accept = 'message/rfc2822'; } debug('%s %s', method, path); var opts = { hostname: this.host, port: this.port, protocol: this.protocol, path: path, method: method, headers: this.headers, auth: this.auth, agent: this.proxy ? proxy(this.proxy, true) : false, timeout: this.timeout }; if (this.retry > 1) { retry(this.retry, function (retryCb) { self.callback = retryCb; self.performRequest(opts); }, fn); } else { this.callback = fn; this.performRequest(opts); } } Request.prototype.request = function (method, resource, data, fn) { if (typeof data === 'function' && !fn) { fn = data; data = {}; } if (!data) { data = {} } return promisifyCall(this, this._request, method, resource, data, fn); }; function getDataValue(key, input) { if (isSpecialParam(key) && (typeof input === 'object')) { return JSON.stringify(input); } else if (typeof input === 'number' || typeof input === 'boolean') { return input.toString(); } else { return input; } } function isSpecialParam(paramKey) { var key = paramKey.toLowerCase(); return ((key === 'vars' || key === 'members' || key === 'recipient-variables') || (key.indexOf('v:') === 0)); } Request.prototype.prepareData = function (data) { var params = {}; for (var key in data) { if (key !== 'attachment' && key !== 'inline' && isOk(data[key])) { var value = getDataValue(key, data[key]); if(isOk(value)) { params[key] = value; } } else { params[key] = data[key]; } } return params; }; Request.prototype.prepareFormData = function (data) { this.form = new FormData(); var self = this; for (var key in data) { var obj = data[key]; if(isOk(obj)) { if (key === 'attachment' || key === 'inline') { if (Array.isArray(obj)) { for (var i = 0; i < obj.length; i++) { this.handleAttachmentObject(key, obj[i]); } } else { this.handleAttachmentObject(key, obj); } } else if (key === 'message') { this.handleMimeObject(key, obj); } else if (Array.isArray(obj)) { function appendKey(element) { if(isOk(element)) { var value = getDataValue(key, element); if(isOk(value)) { self.form.append(key, value); } } } obj.forEach(appendKey); } else { var value = getDataValue(key, obj); if(isOk(value)) { this.form.append(key, value); } } } } this.headers = this.form.getHeaders(); }; Request.prototype.handleMimeObject = function (key, obj) { var self = this; if (typeof obj === 'string') { if (fs.existsSync(obj) && fs.statSync(obj).isFile()) { self.form.append('message', fs.createReadStream(obj)); } else { self.form.append('message', new Buffer(obj), { filename: 'message.mime', contentType: 'message/rfc822', knownLength: obj.length }); } } else if (obj instanceof Readable) { self.form.append('message', obj); } }; Request.prototype.handleAttachmentObject = function (key, obj) { if (!this.form) this.form = new FormData(); if (Buffer.isBuffer(obj)) { debug('appending buffer to form data. key: %s', key); this.form.append(key, obj, { filename: 'file' }); } else if (typeof obj === 'string') { debug('appending stream to form data. key: %s obj: %s', key, obj); this.form.append(key, fs.createReadStream(obj)); } else if ((typeof obj === 'object') && (obj.readable === true)) { debug('appending readable stream to form data. key: %s obj: %s', key, obj); this.form.append(key, obj); } else if ((typeof obj === 'object') && (obj instanceof Attachment)) { var attachmentType = obj.getType(); if (attachmentType === 'path') { debug('appending attachment stream to form data. key: %s data: %s filename: %s', key, obj.data, obj.filename); this.form.append(key, fs.createReadStream(obj.data), { filename: obj.filename || 'attached file' }); } else if (attachmentType === 'buffer') { debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename); var formOpts = { filename: obj.filename || 'attached file' }; if (obj.contentType) { formOpts.contentType = obj.contentType } if (obj.knownLength) { formOpts.knownLength = obj.knownLength } this.form.append(key, obj.data, formOpts); } else if (attachmentType === 'stream') { if (obj.knownLength && obj.contentType) { debug('appending attachment stream to form data. key: %s filename: %s', key, obj.filename); this.form.append(key, obj.data, { filename: obj.filename || 'attached file', contentType: obj.contentType, knownLength: obj.knownLength }); } else { debug('missing content type or length for attachment stream. key: %s', key); } } } else { debug('unknown attachment type. key: %s', key); } }; Request.prototype.handleResponse = function (res) { var self = this; var chunks = ''; var error; res.on('data', function (chunk) { chunks += chunk; }); res.on('error', function (err) { error = err; }); res.on('end', function () { var body; debug('response status code: %s content type: %s error: %s', res.statusCode, res.headers['content-type'], error); // FIXME: An ugly hack to overcome invalid response type in mailgun api (see http://bit.ly/1eF30fU). // We skip content-type validation for 'campaings' endpoint assuming it is JSON. var skipContentTypeCheck = res.req && res.req.path && res.req.path.match(/\/campaigns/); var isJSON = res.headers['content-type'] && res.headers['content-type'].indexOf('application/json') >= 0; if (chunks && !error && (skipContentTypeCheck || isJSON)) { try { body = JSON.parse(chunks); } catch (e) { error = e; } } if (process.env.DEBUG_MAILGUN_FORCE_RETRY) { error = new Error('Force retry error'); delete process.env.DEBUG_MAILGUN_FORCE_RETRY; } if (!error && res.statusCode !== 200) { var msg = body ? body.message || body.response : body || chunks || res.statusMessage; error = new Error(msg); error.statusCode = res.statusCode; } return self.callback(error, body); }); }; Request.prototype.performRequest = function (options) { var self = this; var method = options.method; if (this.form && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { this.form.submit(options, function (err, res) { if (err) { return self.callback(err); } return self.handleResponse(res); }); } else { var req; if (options.protocol === 'http:') { req = http.request(options, function (res) { return self.handleResponse(res); }); } else { req = https.request(options, function (res) { return self.handleResponse(res); }); } if (options.timeout) { req.setTimeout(options.timeout, function () { // timeout occurs req.abort(); }); } req.on('error', function (e) { return self.callback(e); }); if (this.payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { req.write(this.payload); } req.end(); } }; module.exports = Request;