UNPKG

15.5 kBJavaScriptView Raw
1/*
2 * The MIT License (MIT)
3 *
4 * Copyright (c) 2015 Mailjet
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 *
24 */
25
26const DEBUG_MODE = false
27const RESOURCE = 0
28const ID = 1
29const ACTION = 2
30
31/*
32 * Imports.
33 *
34 * qs is used to format the url from the provided parameters and method
35 * _path will join a path according to the OS specifications
36 * https will be used to make a secure http request to the API
37 * fs will simply be used to read files
38 */
39
40const qs = require('querystring')
41const request = require('superagent')
42const _path = require('path')
43const JSONb = require('json-bigint')({ storeAsString: true })
44const version = require('./package.json').version
45
46/* Extend superagent request with proxy method */
47require('superagent-proxy')(request)
48
49/*
50 * MailjetClient constructor.
51 *
52 * @qpi_key (optional) {String} mailjet account api key
53 * @api_secret (optional) {String} mailjet account api secret
54 * @options (optional) {Object} additional connection options
55 *
56 * If you don't know what this is about, sign up to Mailjet at:
57 * https://www.mailjet.com/
58 */
59function MailjetClient (api_key, api_secret, options, perform_api_call) {
60 return this.authStrategy(api_key, api_secret, options, perform_api_call)
61}
62
63/**
64 * @param (optional){String} api_key || api_token
65 * @param (optional){String} api_secret
66 * @param (optional){Object} options
67 * @param (optional){any} perform_api_call
68 */
69MailjetClient.prototype.authStrategy = function(api_key, api_secret, options, perform_api_call) {
70
71 var isTokenRequired = this.isTokenRequired(api_key, api_secret, options, perform_api_call)
72 var self = this
73 // Check if api version requires toekn authentication
74 // This is one of the approaches, maybe there is better
75 if (isTokenRequired) {
76 // params are shifted one position to left as we don't have api secret any more
77 // api_key becomes api_token
78 // api_secret becomes options
79 // options becomes perform_api_call
80 return tokenAuthentication(api_key, api_secret, options)
81 } else {
82 // params are in correct order
83 return basicAuthentication(api_key, api_secret, options, perform_api_call)
84 }
85
86 /**
87 *
88 * @param (optional){String} api_key mailjet api key
89 * @param (optional){String} api_secret mailjet api secret
90 * @param (optional){Object} options additional connection options
91 * @param (optional){boolean} perform_api_call
92 */
93 function basicAuthentication(api_key, api_secret, options, perform_api_call) {
94
95 self.config = self.setConfig(options)
96 self.perform_api_call = perform_api_call || false
97 // To be updated according to the npm repo version
98 self.version = version
99 if (api_key && api_secret) {
100 self.connect(api_key, api_secret, options)
101 }
102
103 return self
104 }
105
106 /**
107 *
108 * @param (optional){String} api_token mailjet api token
109 * @param (optional){Object} options additional connection options
110 * @param (optional){boolean} perform_api_call
111 */
112 function tokenAuthentication(api_token, options, perform_api_call) {
113 self.perform_api_call = perform_api_call || false
114 // To be updated according to the npm repo version
115 self.version = version
116 if (api_token) {
117 self.connect(api_token, options)
118 }
119
120 return self
121 }
122}
123
124MailjetClient.prototype.isTokenRequired = function () {
125 var args = [].slice.call(arguments)
126 var vals = args.filter(a => a !== undefined)
127
128 if (DEBUG_MODE) {
129 console.log('Defined arguments: ' + JSON.stringify(vals))
130 }
131
132 return (vals.length === 1 || (vals.length >= 2 && typeof vals[1] === 'object'))
133}
134
135MailjetClient.prototype.typeJson = function (body) {
136 var keys = Object.keys(body)
137 for (var i in keys) {
138 var key = keys[i]
139 body[key] = parseInt(body[key]) || body[key]
140 }
141 return body
142}
143
144/*
145 * [Static] connect.
146 *
147 * Return a nez connected instance of the MailjetClient class
148 *
149 * @k {String} mailjet qpi key
150 * @s {String} mailjet api secret
151 * @o {String} optional connection options
152 *
153 */
154MailjetClient.connect = function (k, s, o) {
155 return new MailjetClient().connect(k, s, o)
156}
157
158/*
159 * connect.
160 *
161 * create a auth property from the api key and secret
162 *
163 * @apiKey || @apiToken {String}
164 * @apiSecret {String}
165 * @options {Object}
166 *
167 */
168MailjetClient.prototype.connect = function (apiKey, apiSecret, options) {
169 return this.connectStrategy(apiKey, apiSecret, options)
170}
171
172/**
173 * @param (optional){String} apiKey || apiToken
174 * @param (optional){String} apiSecret
175 * @param (optional){Object} options
176 */
177MailjetClient.prototype.connectStrategy = function (apiKey, apiSecret, options) {
178
179 var self = this
180 var isTokenRequired = this.isTokenRequired(apiKey, apiSecret, options)
181
182 if (isTokenRequired) {
183 return tokenConnectStrategy(apiKey, apiSecret)
184 } else {
185 return basicConnectStrategy(apiKey, apiSecret, options)
186 }
187
188 function basicConnectStrategy(apiKey, apiSecret, options) {
189 if (!apiKey) {
190 throw new Error('Mailjet API_KEY is required');
191 }
192 if (!apiSecret) {
193 throw new Error('Mailjet API_SECRET is required');
194 }
195
196 setOptions(options)
197 self.apiKey = apiKey
198 self.apiSecret = apiSecret
199 return self
200 }
201
202 function tokenConnectStrategy(apiToken, options) {
203 setOptions(options)
204 if (!apiToken) {
205 throw new Error('Mailjet API_TOKEN is required');
206 }
207
208 self.apiToken = apiToken
209 return self
210 }
211
212 function setOptions(options) {
213 self.options = options || {}
214 if (self.options) {
215 self.config = self.setConfig(options)
216 }
217 }
218}
219
220MailjetClient.prototype.setConfig = function (options) {
221 const config = require('./config.json')
222 if (typeof options === 'object' && options != null && options.length != 0) {
223 if (options.url) config.url = options.url
224 if (options.version) config.version = options.version
225 if (options.perform_api_call) config.perform_api_call = options.perform_api_call
226 } else if (options != null) {
227 throw new Error('warning, your options variable is not a valid object.')
228 }
229
230 return config
231}
232
233/*
234 * path.
235 *
236 * Returns a formatted url from a http method and
237 * a parameters object literal
238 *
239 * @resource {String}
240 * @sub {String} REST/''/DATA
241 * @params {Object literal} {name: value}
242 *
243 */
244MailjetClient.prototype.path = function (resource, sub, params, options) {
245 if (DEBUG_MODE) {
246 console.log('resource =', resource)
247 console.log('subPath =', sub)
248 console.log('filters =', params)
249 }
250
251 const url = (options && 'url' in options) ? options.url : this.config.url
252 const api_version = (options && 'version' in options) ? options.version : this.config.version
253
254 const base = _path.join(api_version, sub)
255 if (Object.keys(params).length === 0) {
256 return _path.join(url, base + '/' + resource)
257 }
258
259 const querystring = qs.stringify(params);
260 return _path.join(url, base + '/' + resource + '?' + querystring)
261}
262
263/*
264 * httpRequest.
265 *
266 * @method {String} http method (GET/POST...)
267 * @url {String} url path to be used for the request
268 * @data {Object literal} additional data espacially for POST/PUT operations
269 * @callback -optional {Function} called on response from the server, or on error
270 *
271 * @return a promise triggering 'success' on response
272 * and error on error
273 */
274
275MailjetClient.prototype.httpRequest = function (method, url, data, callback, perform_api_call){
276 var req = request[method](url)
277 .set('user-agent', 'mailjet-api-v3-nodejs/' + this.version)
278
279 .set('Content-type', url.indexOf('text:plain') > -1
280 ? 'text/plain'
281 : 'application/json')
282
283 if (this.apiToken) {
284 req.set('Authorization', 'Bearer ' + this.apiToken)
285 } else {
286 req.auth(this.apiKey, this.apiSecret)
287 }
288
289 if (this.options.proxyUrl) {
290 req = req.proxy(this.options.proxyUrl)
291 }
292 if (this.options.timeout) {
293 req = req.timeout(this.options.timeout)
294 }
295
296 const payload = method === 'post' || method === 'put' ? data : {}
297
298 if (DEBUG_MODE) {
299 console.log('Final url: ' + url)
300 console.log('body: ' + JSON.stringify(payload))
301 }
302
303 if (perform_api_call === false || this.perform_api_call) {
304 return Promise.resolve({body: payload, url: url})
305 }
306
307 if (method === 'delete') { method = 'del' }
308 if (method === 'post' || method === 'put') { req = req.send(data) }
309
310 return new Promise(function (resolve, reject) {
311
312 const ret = function (err, result) {
313 return typeof callback === 'function'
314 ? callback(err, result)
315 : err
316 ? reject(err)
317 : resolve(result)
318 }
319
320 req.end(function (err, result) {
321 var body
322
323 try {
324 body = JSONb.parse(result.text)
325 } catch (e) {
326 body = {}
327 }
328
329 if (err) {
330 const error = new Error()
331 error.ErrorMessage = body.ErrorMessage || err.message
332 error.ErrorIdentifier = body.ErrorIdentifier
333 error.statusCode = err.status || null
334 error.response = result || null
335 error.message = 'Unsuccessful: ' + error.statusCode + ' ' + error.ErrorMessage
336 return ret(error)
337 }
338
339 return ret(null, {
340 response: result,
341 body: body
342 })
343 })
344 })
345}
346
347/*
348 *
349 * MailjetResource constructor
350 *
351 * This class creates a function that can be build through method chaining
352 *
353 * @method {String} http method
354 * @func {String} resource/path to be sent
355 * @context {MailjetClient[instance]} parent client
356 */
357function MailjetResource (method, func, options, context) {
358 this.base = func
359 this.callUrl = func
360 this.options = options || context.options
361
362 this.resource = func.toLowerCase()
363
364 this.lastAdded = RESOURCE
365 var self = context
366
367 /*
368 It can be REST or nothing if we only know the resource
369 */
370 this.subPath = (function () {
371 if (func.toLowerCase() !== 'send' && func.indexOf('sms') === -1) {
372 return 'REST'
373 }
374 return ''
375 })()
376
377 /**
378 *
379 * result.
380 *
381 * @params (optional) {Object Littteral} parameters to be sent to the server
382 * @callback (optional) {Function} called on response or error
383 */
384 var that = this
385 this.result = function (params, callback) {
386 params = params || {}
387 if (typeof params === 'function') {
388 callback = params
389 params = {}
390 }
391
392 /*
393 We build the querystring depending on the parameters. if the user explicitly mentionned
394 a filters property, we pass it to the url
395 */
396 var path = self.path(that.callUrl, that.subPath, (function () {
397 if (params.filters) {
398 var ret = params.filters
399 delete params.filters
400 return ret
401 } else if (method === 'get') {
402 return params
403 } else {
404 return {}
405 }
406 })(), that.options)
407
408 var perform_api_call = null
409 if (that.options && 'perform_api_call' in that.options) {
410 perform_api_call = that.options.perform_api_call
411 } else {
412 perform_api_call = self.config.perform_api_call
413 }
414
415 that.callUrl = that.base
416 self.lastAdded = RESOURCE
417 return self.httpRequest(method, 'https://' + path, params, callback, perform_api_call)
418 }
419}
420
421/**
422 *
423 * id.
424 *
425 * Add an ID and prevent invalid id chaining
426 *
427 * @value {String/Number} append an id to the path
428 * @return the MailjetResource instance to allow method chaining
429 *
430 */
431MailjetResource.prototype.id = function (value) {
432 if (this.lastAdded === ID && DEBUG_MODE) {
433 console.warn('[WARNING] your request may fail due to invalid id chaining')
434 }
435
436 this.callUrl = _path.join(this.callUrl, value.toString())
437 this.lastAdded = ID
438 return this
439}
440
441/**
442 *
443 * action.
444 *
445 * Add an Action and prevent invalid action chaining
446 *
447 * @value {String} append an action to the path
448 * @return the MailjetResource instance to allow method chaining
449 *
450 */
451MailjetResource.prototype.action = function (name) {
452 if (this.lastAdded === ACTION && DEBUG_MODE) {
453 console.warn('[WARNING] your request may fail due to invalid action chaining')
454 }
455
456 this.callUrl = _path.join(this.callUrl, name)
457 this.action = name.toLowerCase()
458
459 this.lastAdded = ACTION
460
461 if (this.action.toLowerCase() === 'csvdata') {
462 this.action = 'csvdata/text:plain'
463 } else if (this.action.toLowerCase() === 'csverror') {
464 this.action = 'csverror/text:csv'
465 }
466
467 var self = this
468 this.subPath = (function () {
469 if (self.resource === 'contactslist' && self.action === 'csvdata/text:plain' ||
470 self.resource === 'batchjob' && self.action === 'csverror/text:csv') {
471 return 'DATA'
472 } else {
473 return self.subPath
474 }
475 })()
476 return self
477}
478
479/**
480 *
481 * request.
482 *
483 * @parmas {Object literal} method parameters
484 * @callback (optional) {Function} triggered when done
485 *
486 * @return {String} the server response
487 */
488
489MailjetResource.prototype.request = function (params, callback) {
490 return this.result(params, callback).catch(function (err) {
491 try {
492 const ErrorDetails = JSON.parse(err.response.text);
493 const fullMessage = ErrorDetails.Messages[0].Errors[0].ErrorMessage;
494
495 if (typeof fullMessage === "string") {
496 err.message = err.message + ";\n" + fullMessage;
497 throw err;
498 }
499 throw err;
500 } catch {
501 throw err;
502 }
503 });
504};
505
506/*
507 * post.
508 *
509 * @func {String} required Mailjet API function to be used (can contain a whole action path)
510 *
511 * @returns a function that make an httpRequest for each call
512 */
513MailjetClient.prototype.post = function (func, options) {
514 return new MailjetResource('post', func, options, this)
515}
516
517/*
518 * get.
519 *
520 * @func {String} required Mailjet API function to be used (can contain a whole action path)
521 *
522 * @returns a function that make an httpRequest for each call
523 */
524MailjetClient.prototype.get = function (func, options) {
525 return new MailjetResource('get', func, options, this)
526}
527
528/*
529 * delete.
530 *
531 * @func {String} required Mailjet API function to be used (can contain a whole action path)
532 *
533 * @returns a function that make an httpRequest for each call
534 */
535MailjetClient.prototype.delete = function (func, options) {
536 return new MailjetResource('delete', func, options, this)
537}
538
539/*
540 * put.
541 *
542 * @func {String} required Mailjet API function to be used (can contain a whole action path)
543 *
544 * @returns a function that make an httpRequest for each call
545 */
546MailjetClient.prototype.put = function (func, options) {
547 return new MailjetResource('put', func, options, this)
548}
549
550/*
551 * Exports the Mailjet client.
552 *
553 * you can require it like so:
554 * var mj = require ('./mailjet-client')
555 *
556 * or for the bleeding edge developpers out there:
557 * import mj from './mailjet-client'
558 */
559module.exports = MailjetClient