UNPKG

15.8 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;
45const setValueIfExist = require('./lib/utils/setValueIfExist');
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
136/*
137 * [Static] connect.
138 *
139 * Return a nez connected instance of the MailjetClient class
140 *
141 * @k {String} mailjet qpi key
142 * @s {String} mailjet api secret
143 * @o {String} optional connection options
144 *
145 */
146MailjetClient.connect = function (k, s, o) {
147 return new MailjetClient().connect(k, s, o);
148};
149
150/*
151 * connect.
152 *
153 * create a auth property from the api key and secret
154 *
155 * @apiKey || @apiToken {String}
156 * @apiSecret {String}
157 * @options {Object}
158 *
159 */
160MailjetClient.prototype.connect = function (apiKey, apiSecret, options) {
161 return this.connectStrategy(apiKey, apiSecret, options);
162};
163
164/**
165 * @param (optional){String} apiKey || apiToken
166 * @param (optional){String} apiSecret
167 * @param (optional){Object} options
168 */
169MailjetClient.prototype.connectStrategy = function (apiKey, apiSecret, options) {
170
171 var self = this;
172 var isTokenRequired = this.isTokenRequired(apiKey, apiSecret, options);
173
174 if (isTokenRequired) {
175 return tokenConnectStrategy(apiKey, apiSecret);
176 } else {
177 return basicConnectStrategy(apiKey, apiSecret, options);
178 }
179
180 function basicConnectStrategy(apiKey, apiSecret, options) {
181 if (!apiKey) {
182 throw new Error('Mailjet API_KEY is required');
183 }
184 if (!apiSecret) {
185 throw new Error('Mailjet API_SECRET is required');
186 }
187
188 setOptions(options);
189 self.apiKey = apiKey;
190 self.apiSecret = apiSecret;
191
192 return self;
193 }
194
195 function tokenConnectStrategy(apiToken, options) {
196 if (!apiToken) {
197 throw new Error('Mailjet API_TOKEN is required');
198 }
199
200 setOptions(options);
201 self.apiToken = apiToken;
202
203 return self;
204 }
205
206 function setOptions(options) {
207 self.options = options || {};
208 if (self.options) {
209 self.config = self.setConfig(options);
210 }
211 }
212};
213
214MailjetClient.prototype.setConfig = function (options) {
215 const config = { ...require('./config.json') };
216 if (typeof options === 'object' && options !== null) {
217 if (options.url) config.url = options.url;
218 if (options.version) config.version = options.version;
219 if ('perform_api_call' in options) config.perform_api_call = options.perform_api_call;
220 } else if (options != null) { // TODO: Bug -> must be strict equal
221 throw new Error('warning, your options variable is not a valid object.');
222 }
223
224 return config;
225};
226
227/*
228 * path.
229 *
230 * Returns a formatted url from a http method and
231 * a parameters object literal
232 *
233 * @resource {String}
234 * @sub {String} REST/''/DATA
235 * @params {Object literal} {name: value}
236 *
237 */
238MailjetClient.prototype.path = function (resource, sub, params, options) {
239 if (DEBUG_MODE) {
240 console.log('resource =', resource);
241 console.log('subPath =', sub);
242 console.log('filters =', params);
243 }
244
245 const url = (options && 'url' in options) ? options.url : this.config.url;
246 const api_version = (options && 'version' in options) ? options.version : this.config.version;
247
248 const base = _path.join(api_version, sub);
249 const path = _path.join(url, base + '/' + resource);
250
251 if (Object.keys(params).length === 0) {
252 return path;
253 }
254
255 const querystring = qs.stringify(params);
256 return `${path}?${querystring}`;
257};
258
259/*
260 * httpRequest.
261 *
262 * @method {String} http method (GET/POST...)
263 * @url {String} url path to be used for the request
264 * @data {Object literal} additional data espacially for POST/PUT operations
265 * @callback -optional {Function} called on response from the server, or on error
266 *
267 * @return a promise triggering 'success' on response
268 * and error on error
269 */
270
271MailjetClient.prototype.httpRequest = function (method, url, data, callback, perform_api_call){
272 var req = request[method](url)
273 .set('user-agent', 'mailjet-api-v3-nodejs/' + this.version)
274
275 .set('Content-type', url.indexOf('text:plain') > -1
276 ? 'text/plain'
277 : 'application/json');
278
279 if (this.apiToken) {
280 req.set('Authorization', 'Bearer ' + this.apiToken);
281 } else {
282 req.auth(this.apiKey, this.apiSecret);
283 }
284
285 if (this.options.proxyUrl) {
286 req = req.proxy(this.options.proxyUrl);
287 }
288 if (this.options.timeout) {
289 req = req.timeout(this.options.timeout);
290 }
291
292 const payload = method === 'post' || method === 'put' ? data : {};
293
294 if (DEBUG_MODE) {
295 console.log('Final url: ' + url);
296 console.log('body: ' + JSON.stringify(payload));
297 }
298
299 if (perform_api_call === false || this.perform_api_call) {
300 return Promise.resolve({body: payload, url: url});
301 }
302
303 if (method === 'delete') { method = 'del'; }
304 if (method === 'post' || method === 'put') { req = req.send(data); }
305
306 return new Promise(function (resolve, reject) {
307
308 const ret = function (err, result) {
309 return typeof callback === 'function'
310 ? callback(err, result)
311 : err
312 ? reject(err)
313 : resolve(result);
314 };
315
316 req.end(function (err, result) {
317 var body;
318
319 try {
320 body = JSONb.parse(result.text);
321 } catch (e) {
322 body = {};
323 }
324
325 if (err) {
326 const error = new Error();
327 error.ErrorMessage = body.ErrorMessage || err.message;
328
329 error.statusCode = err.status || null;
330 error.response = result || null;
331 error.message = `Unsuccessful: Status Code: "${error.statusCode}" Message: "${error.ErrorMessage}"`;
332
333 setValueIfExist(error, 'ErrorIdentifier', body.ErrorIdentifier);
334 setValueIfExist(error, 'ErrorCode', body.ErrorCode);
335 setValueIfExist(error, 'ErrorRelatedTo', body.ErrorRelatedTo);
336
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.action = name.toLowerCase();
458 this.lastAdded = ACTION;
459
460 if (this.action.toLowerCase() === 'csvdata') {
461 this.action = 'csvdata/text:plain';
462 } else if (this.action.toLowerCase() === 'csverror') {
463 this.action = 'csverror/text:csv';
464 }
465
466 this.callUrl = _path.join(this.callUrl, this.action);
467
468 var self = this;
469 this.subPath = (function () {
470 const isContactListWithCSV = self.resource === 'contactslist' && self.action === 'csvdata/text:plain';
471 const isBatchJobWithCSV = self.resource === 'batchjob' && self.action === 'csverror/text:csv';
472
473 return (isContactListWithCSV || isBatchJobWithCSV) ? 'DATA' : self.subPath;
474 })();
475 return self;
476};
477
478/**
479 *
480 * request.
481 *
482 * @parmas {Object literal} method parameters
483 * @callback (optional) {Function} triggered when done
484 *
485 * @return {String} the server response
486 */
487
488MailjetResource.prototype.request = function (params, callback) {
489 return this.result(params, callback).catch(function (err) {
490 try {
491 const ErrorDetails = JSON.parse(err.response.text);
492 const fullMessage = ErrorDetails.Messages[0].Errors[0].ErrorMessage;
493
494 if (typeof fullMessage === 'string') {
495 err.message = err.message + ';\n' + fullMessage;
496 }
497
498 throw err;
499 } catch {
500 throw err;
501 }
502 });
503};
504
505/*
506 * post.
507 *
508 * @func {String} required Mailjet API function to be used (can contain a whole action path)
509 *
510 * @returns a function that make an httpRequest for each call
511 */
512MailjetClient.prototype.post = function (func, options) {
513 return new MailjetResource('post', func, options, this);
514};
515
516/*
517 * get.
518 *
519 * @func {String} required Mailjet API function to be used (can contain a whole action path)
520 *
521 * @returns a function that make an httpRequest for each call
522 */
523MailjetClient.prototype.get = function (func, options) {
524 return new MailjetResource('get', func, options, this);
525};
526
527/*
528 * delete.
529 *
530 * @func {String} required Mailjet API function to be used (can contain a whole action path)
531 *
532 * @returns a function that make an httpRequest for each call
533 */
534MailjetClient.prototype.delete = function (func, options) {
535 return new MailjetResource('delete', func, options, this);
536};
537
538/*
539 * put.
540 *
541 * @func {String} required Mailjet API function to be used (can contain a whole action path)
542 *
543 * @returns a function that make an httpRequest for each call
544 */
545MailjetClient.prototype.put = function (func, options) {
546 return new MailjetResource('put', func, options, this);
547};
548
549/*
550 * Exports the Mailjet client.
551 *
552 * you can require it like so:
553 * var mj = require('./mailjet-client').MailjetClient
554 *
555 * or for the bleeding edge developpers out there:
556 * import { MailjetClient } from './mailjet-client'
557 */
558module.exports.MailjetClient = MailjetClient;
559module.exports.MailjetResource = MailjetResource;
\No newline at end of file