source: node_modules/nodemailer/lib/shared/index.js@ 62b2964

finki-main main
Last change on this file since 62b2964 was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

  • Property mode set to 100644
File size: 19.7 KB
Line 
1/* eslint no-console: 0 */
2
3'use strict';
4
5const urllib = require('url');
6const util = require('util');
7const fs = require('fs');
8const nmfetch = require('../fetch');
9const dns = require('dns');
10const net = require('net');
11const os = require('os');
12
13const DNS_TTL = 5 * 60 * 1000;
14
15let networkInterfaces;
16try {
17 networkInterfaces = os.networkInterfaces();
18} catch (err) {
19 // fails on some systems
20}
21
22module.exports.networkInterfaces = networkInterfaces;
23
24const isFamilySupported = (family, allowInternal) => {
25 let networkInterfaces = module.exports.networkInterfaces;
26 if (!networkInterfaces) {
27 // hope for the best
28 return true;
29 }
30
31 const familySupported =
32 // crux that replaces Object.values(networkInterfaces) as Object.values is not supported in nodejs v6
33 Object.keys(networkInterfaces)
34 .map(key => networkInterfaces[key])
35 // crux that replaces .flat() as it is not supported in older Node versions (v10 and older)
36 .reduce((acc, val) => acc.concat(val), [])
37 .filter(i => !i.internal || allowInternal)
38 .filter(i => i.family === 'IPv' + family || i.family === family).length > 0;
39
40 return familySupported;
41};
42
43const resolver = (family, hostname, options, callback) => {
44 options = options || {};
45 const familySupported = isFamilySupported(family, options.allowInternalNetworkInterfaces);
46
47 if (!familySupported) {
48 return callback(null, []);
49 }
50
51 const resolver = dns.Resolver ? new dns.Resolver(options) : dns;
52 resolver['resolve' + family](hostname, (err, addresses) => {
53 if (err) {
54 switch (err.code) {
55 case dns.NODATA:
56 case dns.NOTFOUND:
57 case dns.NOTIMP:
58 case dns.SERVFAIL:
59 case dns.CONNREFUSED:
60 case dns.REFUSED:
61 case 'EAI_AGAIN':
62 return callback(null, []);
63 }
64 return callback(err);
65 }
66 return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
67 });
68};
69
70const dnsCache = (module.exports.dnsCache = new Map());
71
72const formatDNSValue = (value, extra) => {
73 if (!value) {
74 return Object.assign({}, extra || {});
75 }
76
77 return Object.assign(
78 {
79 servername: value.servername,
80 host:
81 !value.addresses || !value.addresses.length
82 ? null
83 : value.addresses.length === 1
84 ? value.addresses[0]
85 : value.addresses[Math.floor(Math.random() * value.addresses.length)]
86 },
87 extra || {}
88 );
89};
90
91module.exports.resolveHostname = (options, callback) => {
92 options = options || {};
93
94 if (!options.host && options.servername) {
95 options.host = options.servername;
96 }
97
98 if (!options.host || net.isIP(options.host)) {
99 // nothing to do here
100 let value = {
101 addresses: [options.host],
102 servername: options.servername || false
103 };
104 return callback(
105 null,
106 formatDNSValue(value, {
107 cached: false
108 })
109 );
110 }
111
112 let cached;
113 if (dnsCache.has(options.host)) {
114 cached = dnsCache.get(options.host);
115
116 if (!cached.expires || cached.expires >= Date.now()) {
117 return callback(
118 null,
119 formatDNSValue(cached.value, {
120 cached: true
121 })
122 );
123 }
124 }
125
126 resolver(4, options.host, options, (err, addresses) => {
127 if (err) {
128 if (cached) {
129 // ignore error, use expired value
130 return callback(
131 null,
132 formatDNSValue(cached.value, {
133 cached: true,
134 error: err
135 })
136 );
137 }
138 return callback(err);
139 }
140
141 if (addresses && addresses.length) {
142 let value = {
143 addresses,
144 servername: options.servername || options.host
145 };
146
147 dnsCache.set(options.host, {
148 value,
149 expires: Date.now() + (options.dnsTtl || DNS_TTL)
150 });
151
152 return callback(
153 null,
154 formatDNSValue(value, {
155 cached: false
156 })
157 );
158 }
159
160 resolver(6, options.host, options, (err, addresses) => {
161 if (err) {
162 if (cached) {
163 // ignore error, use expired value
164 return callback(
165 null,
166 formatDNSValue(cached.value, {
167 cached: true,
168 error: err
169 })
170 );
171 }
172 return callback(err);
173 }
174
175 if (addresses && addresses.length) {
176 let value = {
177 addresses,
178 servername: options.servername || options.host
179 };
180
181 dnsCache.set(options.host, {
182 value,
183 expires: Date.now() + (options.dnsTtl || DNS_TTL)
184 });
185
186 return callback(
187 null,
188 formatDNSValue(value, {
189 cached: false
190 })
191 );
192 }
193
194 try {
195 dns.lookup(options.host, { all: true }, (err, addresses) => {
196 if (err) {
197 if (cached) {
198 // ignore error, use expired value
199 return callback(
200 null,
201 formatDNSValue(cached.value, {
202 cached: true,
203 error: err
204 })
205 );
206 }
207 return callback(err);
208 }
209
210 let address = addresses
211 ? addresses
212 .filter(addr => isFamilySupported(addr.family))
213 .map(addr => addr.address)
214 .shift()
215 : false;
216
217 if (addresses && addresses.length && !address) {
218 // there are addresses but none can be used
219 console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
220 }
221
222 if (!address && cached) {
223 // nothing was found, fallback to cached value
224 return callback(
225 null,
226 formatDNSValue(cached.value, {
227 cached: true
228 })
229 );
230 }
231
232 let value = {
233 addresses: address ? [address] : [options.host],
234 servername: options.servername || options.host
235 };
236
237 dnsCache.set(options.host, {
238 value,
239 expires: Date.now() + (options.dnsTtl || DNS_TTL)
240 });
241
242 return callback(
243 null,
244 formatDNSValue(value, {
245 cached: false
246 })
247 );
248 });
249 } catch (err) {
250 if (cached) {
251 // ignore error, use expired value
252 return callback(
253 null,
254 formatDNSValue(cached.value, {
255 cached: true,
256 error: err
257 })
258 );
259 }
260 return callback(err);
261 }
262 });
263 });
264};
265/**
266 * Parses connection url to a structured configuration object
267 *
268 * @param {String} str Connection url
269 * @return {Object} Configuration object
270 */
271module.exports.parseConnectionUrl = str => {
272 str = str || '';
273 let options = {};
274
275 [urllib.parse(str, true)].forEach(url => {
276 let auth;
277
278 switch (url.protocol) {
279 case 'smtp:':
280 options.secure = false;
281 break;
282 case 'smtps:':
283 options.secure = true;
284 break;
285 case 'direct:':
286 options.direct = true;
287 break;
288 }
289
290 if (!isNaN(url.port) && Number(url.port)) {
291 options.port = Number(url.port);
292 }
293
294 if (url.hostname) {
295 options.host = url.hostname;
296 }
297
298 if (url.auth) {
299 auth = url.auth.split(':');
300
301 if (!options.auth) {
302 options.auth = {};
303 }
304
305 options.auth.user = auth.shift();
306 options.auth.pass = auth.join(':');
307 }
308
309 Object.keys(url.query || {}).forEach(key => {
310 let obj = options;
311 let lKey = key;
312 let value = url.query[key];
313
314 if (!isNaN(value)) {
315 value = Number(value);
316 }
317
318 switch (value) {
319 case 'true':
320 value = true;
321 break;
322 case 'false':
323 value = false;
324 break;
325 }
326
327 // tls is nested object
328 if (key.indexOf('tls.') === 0) {
329 lKey = key.substr(4);
330 if (!options.tls) {
331 options.tls = {};
332 }
333 obj = options.tls;
334 } else if (key.indexOf('.') >= 0) {
335 // ignore nested properties besides tls
336 return;
337 }
338
339 if (!(lKey in obj)) {
340 obj[lKey] = value;
341 }
342 });
343 });
344
345 return options;
346};
347
348module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
349 let entry = {};
350
351 Object.keys(defaults || {}).forEach(key => {
352 if (key !== 'level') {
353 entry[key] = defaults[key];
354 }
355 });
356
357 Object.keys(data || {}).forEach(key => {
358 if (key !== 'level') {
359 entry[key] = data[key];
360 }
361 });
362
363 logger[level](entry, message, ...args);
364};
365
366/**
367 * Returns a bunyan-compatible logger interface. Uses either provided logger or
368 * creates a default console logger
369 *
370 * @param {Object} [options] Options object that might include 'logger' value
371 * @return {Object} bunyan compatible logger
372 */
373module.exports.getLogger = (options, defaults) => {
374 options = options || {};
375
376 let response = {};
377 let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
378
379 if (!options.logger) {
380 // use vanity logger
381 levels.forEach(level => {
382 response[level] = () => false;
383 });
384 return response;
385 }
386
387 let logger = options.logger;
388
389 if (options.logger === true) {
390 // create console logger
391 logger = createDefaultLogger(levels);
392 }
393
394 levels.forEach(level => {
395 response[level] = (data, message, ...args) => {
396 module.exports._logFunc(logger, level, defaults, data, message, ...args);
397 };
398 });
399
400 return response;
401};
402
403/**
404 * Wrapper for creating a callback that either resolves or rejects a promise
405 * based on input
406 *
407 * @param {Function} resolve Function to run if callback is called
408 * @param {Function} reject Function to run if callback ends with an error
409 */
410module.exports.callbackPromise = (resolve, reject) =>
411 function () {
412 let args = Array.from(arguments);
413 let err = args.shift();
414 if (err) {
415 reject(err);
416 } else {
417 resolve(...args);
418 }
419 };
420
421module.exports.parseDataURI = uri => {
422 let input = uri;
423 let commaPos = input.indexOf(',');
424 if (!commaPos) {
425 return uri;
426 }
427
428 let data = input.substring(commaPos + 1);
429 let metaStr = input.substring('data:'.length, commaPos);
430
431 let encoding;
432
433 let metaEntries = metaStr.split(';');
434 let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false;
435 if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) {
436 encoding = lastMetaEntry.toLowerCase();
437 metaEntries.pop();
438 }
439
440 let contentType = metaEntries.shift() || 'application/octet-stream';
441 let params = {};
442 for (let entry of metaEntries) {
443 let sep = entry.indexOf('=');
444 if (sep >= 0) {
445 let key = entry.substring(0, sep);
446 let value = entry.substring(sep + 1);
447 params[key] = value;
448 }
449 }
450
451 switch (encoding) {
452 case 'base64':
453 data = Buffer.from(data, 'base64');
454 break;
455 case 'utf8':
456 data = Buffer.from(data);
457 break;
458 default:
459 try {
460 data = Buffer.from(decodeURIComponent(data));
461 } catch (err) {
462 data = Buffer.from(data);
463 }
464 data = Buffer.from(data);
465 }
466
467 return { data, encoding, contentType, params };
468};
469
470/**
471 * Resolves a String or a Buffer value for content value. Useful if the value
472 * is a Stream or a file or an URL. If the value is a Stream, overwrites
473 * the stream object with the resolved value (you can't stream a value twice).
474 *
475 * This is useful when you want to create a plugin that needs a content value,
476 * for example the `html` or `text` value as a String or a Buffer but not as
477 * a file path or an URL.
478 *
479 * @param {Object} data An object or an Array you want to resolve an element for
480 * @param {String|Number} key Property name or an Array index
481 * @param {Function} callback Callback function with (err, value)
482 */
483module.exports.resolveContent = (data, key, callback) => {
484 let promise;
485
486 if (!callback) {
487 promise = new Promise((resolve, reject) => {
488 callback = module.exports.callbackPromise(resolve, reject);
489 });
490 }
491
492 let content = (data && data[key] && data[key].content) || data[key];
493 let contentStream;
494 let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
495 .toString()
496 .toLowerCase()
497 .replace(/[-_\s]/g, '');
498
499 if (!content) {
500 return callback(null, content);
501 }
502
503 if (typeof content === 'object') {
504 if (typeof content.pipe === 'function') {
505 return resolveStream(content, (err, value) => {
506 if (err) {
507 return callback(err);
508 }
509 // we can't stream twice the same content, so we need
510 // to replace the stream object with the streaming result
511 if (data[key].content) {
512 data[key].content = value;
513 } else {
514 data[key] = value;
515 }
516 callback(null, value);
517 });
518 } else if (/^https?:\/\//i.test(content.path || content.href)) {
519 contentStream = nmfetch(content.path || content.href);
520 return resolveStream(contentStream, callback);
521 } else if (/^data:/i.test(content.path || content.href)) {
522 let parsedDataUri = module.exports.parseDataURI(content.path || content.href);
523
524 if (!parsedDataUri || !parsedDataUri.data) {
525 return callback(null, Buffer.from(0));
526 }
527 return callback(null, parsedDataUri.data);
528 } else if (content.path) {
529 return resolveStream(fs.createReadStream(content.path), callback);
530 }
531 }
532
533 if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
534 content = Buffer.from(data[key].content, encoding);
535 }
536
537 // default action, return as is
538 setImmediate(() => callback(null, content));
539
540 return promise;
541};
542
543/**
544 * Copies properties from source objects to target objects
545 */
546module.exports.assign = function (/* target, ... sources */) {
547 let args = Array.from(arguments);
548 let target = args.shift() || {};
549
550 args.forEach(source => {
551 Object.keys(source || {}).forEach(key => {
552 if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
553 // tls and auth are special keys that need to be enumerated separately
554 // other objects are passed as is
555 if (!target[key]) {
556 // ensure that target has this key
557 target[key] = {};
558 }
559 Object.keys(source[key]).forEach(subKey => {
560 target[key][subKey] = source[key][subKey];
561 });
562 } else {
563 target[key] = source[key];
564 }
565 });
566 });
567 return target;
568};
569
570module.exports.encodeXText = str => {
571 // ! 0x21
572 // + 0x2B
573 // = 0x3D
574 // ~ 0x7E
575 if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
576 return str;
577 }
578 let buf = Buffer.from(str);
579 let result = '';
580 for (let i = 0, len = buf.length; i < len; i++) {
581 let c = buf[i];
582 if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
583 result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
584 } else {
585 result += String.fromCharCode(c);
586 }
587 }
588 return result;
589};
590
591/**
592 * Streams a stream value into a Buffer
593 *
594 * @param {Object} stream Readable stream
595 * @param {Function} callback Callback function with (err, value)
596 */
597function resolveStream(stream, callback) {
598 let responded = false;
599 let chunks = [];
600 let chunklen = 0;
601
602 stream.on('error', err => {
603 if (responded) {
604 return;
605 }
606
607 responded = true;
608 callback(err);
609 });
610
611 stream.on('readable', () => {
612 let chunk;
613 while ((chunk = stream.read()) !== null) {
614 chunks.push(chunk);
615 chunklen += chunk.length;
616 }
617 });
618
619 stream.on('end', () => {
620 if (responded) {
621 return;
622 }
623 responded = true;
624
625 let value;
626
627 try {
628 value = Buffer.concat(chunks, chunklen);
629 } catch (E) {
630 return callback(E);
631 }
632 callback(null, value);
633 });
634}
635
636/**
637 * Generates a bunyan-like logger that prints to console
638 *
639 * @returns {Object} Bunyan logger instance
640 */
641function createDefaultLogger(levels) {
642 let levelMaxLen = 0;
643 let levelNames = new Map();
644 levels.forEach(level => {
645 if (level.length > levelMaxLen) {
646 levelMaxLen = level.length;
647 }
648 });
649
650 levels.forEach(level => {
651 let levelName = level.toUpperCase();
652 if (levelName.length < levelMaxLen) {
653 levelName += ' '.repeat(levelMaxLen - levelName.length);
654 }
655 levelNames.set(level, levelName);
656 });
657
658 let print = (level, entry, message, ...args) => {
659 let prefix = '';
660 if (entry) {
661 if (entry.tnx === 'server') {
662 prefix = 'S: ';
663 } else if (entry.tnx === 'client') {
664 prefix = 'C: ';
665 }
666
667 if (entry.sid) {
668 prefix = '[' + entry.sid + '] ' + prefix;
669 }
670
671 if (entry.cid) {
672 prefix = '[#' + entry.cid + '] ' + prefix;
673 }
674 }
675
676 message = util.format(message, ...args);
677 message.split(/\r?\n/).forEach(line => {
678 console.log('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
679 });
680 };
681
682 let logger = {};
683 levels.forEach(level => {
684 logger[level] = print.bind(null, level);
685 });
686
687 return logger;
688}
Note: See TracBrowser for help on using the repository browser.