const tsscmp = require('tsscmp') const crypto = require('crypto') const Attachment = require('./attachment') const Request = require('./request') const builder = require('./build') const resources = require('./schema').definitions const mailgunExpirey = 15 * 60 * 1000 const mailgunHashType = 'sha256' const mailgunSignatureEncoding = 'hex' class Mailgun { constructor (options) { if (!options.apiKey) { throw new Error('apiKey value must be defined!') } this.username = 'api' this.apiKey = options.apiKey this.publicApiKey = options.publicApiKey this.domain = options.domain this.auth = [this.username, this.apiKey].join(':') this.mute = options.mute || false this.timeout = options.timeout this.host = options.host || 'api.mailgun.net' this.endpoint = options.endpoint || '/v3' this.protocol = options.protocol || 'https:' this.port = options.port || 443 this.retry = options.retry || 1 if (options.proxy) { this.proxy = options.proxy } this.options = { 'host': this.host, 'endpoint': this.endpoint, 'protocol': this.protocol, 'port': this.port, 'auth': this.auth, 'proxy': this.proxy, 'timeout': this.timeout, 'retry': this.retry } this.mailgunTokens = {} } getDomain (method, resource) { let d = this.domain // filter out API calls that do not require a domain specified if ((resource.indexOf('/routes') >= 0) || (resource.indexOf('/lists') >= 0) || (resource.indexOf('/address') >= 0) || (resource.indexOf('/domains') >= 0)) { d = '' } else if ((resource.indexOf('/messages') >= 0) && (method === 'GET' || method === 'DELETE')) { d = `domains/${this.domain}` } return d } getRequestOptions (resource) { let o = this.options // use public API key if we have it for the routes that require it if ((resource.indexOf('/address/validate') >= 0 || (resource.indexOf('/address/parse') >= 0)) && this.publicApiKey) { const copy = Object.assign({}, this.options) copy.auth = [this.username, this.publicApiKey].join(':') o = copy } return o } request (method, resource, data, fn) { let fullpath = resource const domain = this.getDomain(method, resource) if (domain) { fullpath = '/'.concat(domain, resource) } const req = new Request(this.options) return req.request(method, fullpath, data, fn) } post (path, data, fn) { const req = new Request(this.options) return req.request('POST', path, data, fn) } get (path, data, fn) { const req = new Request(this.options) return req.request('GET', path, data, fn) } delete (path, data, fn) { const req = new Request(this.options) return req.request('DELETE', path, data, fn) } put (path, data, fn) { const req = new Request(this.options) return req.request('PUT', path, data, fn) } validateWebhook (timestamp, token, signature) { const adjustedTimestamp = parseInt(timestamp, 10) * 1000 const fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey) if (!fresh) { if (!this.mute) { console.error('[mailgun] Stale Timestamp: this may be an attack') console.error('[mailgun] However, this is most likely your fault\n') console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n') console.error(`[mailgun] System Time: ${new Date().toString()}`) console.error(`[mailgun] Mailgun Time: ${new Date(adjustedTimestamp).toString()}`, timestamp) console.error(`[mailgun] Delta: ${Date.now() - adjustedTimestamp}`) } return false } if (this.mailgunTokens[token]) { if (!this.mute) { console.error('[mailgun] Replay Attack') } return false } this.mailgunTokens[token] = true const tokenTimeout = setTimeout(() => { delete this.mailgunTokens[token] }, mailgunExpirey + (5 * 1000)) tokenTimeout.unref() return tsscmp( signature, crypto.createHmac(mailgunHashType, this.apiKey) .update(Buffer.from(timestamp + token, 'utf-8')) .digest(mailgunSignatureEncoding) ) } validate (address, isPrivate, opts, fn) { if (typeof opts === 'function') { fn = opts opts = {} } if (typeof isPrivate === 'object') { opts = isPrivate isPrivate = false } if (typeof isPrivate === 'function') { fn = isPrivate isPrivate = false opts = {} } let resource = '/address/validate' if (isPrivate) { resource = '/address/private/validate' } const options = this.getRequestOptions(resource) const req = new Request(options) const data = Object.assign({}, { address }, opts) return req.request('GET', resource, data, fn) } parse (addresses, isPrivate, opts, fn) { if (typeof opts === 'function') { fn = opts opts = {} } if (typeof isPrivate === 'object') { opts = isPrivate isPrivate = false } if (typeof isPrivate === 'function') { fn = isPrivate isPrivate = false opts = {} } let resource = '/address/parse' if (isPrivate) { resource = '/address/private/parse' } const options = this.getRequestOptions(resource) const req = new Request(options) const data = Object.assign({}, { addresses }, opts) return req.request('GET', resource, data, fn) } } builder.build(Mailgun, resources) Mailgun.prototype.Attachment = Attachment Mailgun.prototype.Mailgun = Mailgun function create (options) { return new Mailgun(options) } module.exports = create