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