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