| 'use strict'; |
| |
| const urllib = require('url'); |
| const util = require('util'); |
| const fs = require('fs'); |
| const fetch = require('../fetch'); |
| |
| /** |
| * Parses connection url to a structured configuration object |
| * |
| * @param {String} str Connection url |
| * @return {Object} Configuration object |
| */ |
| module.exports.parseConnectionUrl = str => { |
| str = str || ''; |
| let options = {}; |
| |
| [urllib.parse(str, true)].forEach(url => { |
| let auth; |
| |
| switch (url.protocol) { |
| case 'smtp:': |
| options.secure = false; |
| break; |
| case 'smtps:': |
| options.secure = true; |
| break; |
| case 'direct:': |
| options.direct = true; |
| break; |
| } |
| |
| if (!isNaN(url.port) && Number(url.port)) { |
| options.port = Number(url.port); |
| } |
| |
| if (url.hostname) { |
| options.host = url.hostname; |
| } |
| |
| if (url.auth) { |
| auth = url.auth.split(':'); |
| |
| if (!options.auth) { |
| options.auth = {}; |
| } |
| |
| options.auth.user = auth.shift(); |
| options.auth.pass = auth.join(':'); |
| } |
| |
| Object.keys(url.query || {}).forEach(key => { |
| let obj = options; |
| let lKey = key; |
| let value = url.query[key]; |
| |
| if (!isNaN(value)) { |
| value = Number(value); |
| } |
| |
| switch (value) { |
| case 'true': |
| value = true; |
| break; |
| case 'false': |
| value = false; |
| break; |
| } |
| |
| // tls is nested object |
| if (key.indexOf('tls.') === 0) { |
| lKey = key.substr(4); |
| if (!options.tls) { |
| options.tls = {}; |
| } |
| obj = options.tls; |
| } else if (key.indexOf('.') >= 0) { |
| // ignore nested properties besides tls |
| return; |
| } |
| |
| if (!(lKey in obj)) { |
| obj[lKey] = value; |
| } |
| }); |
| }); |
| |
| return options; |
| }; |
| |
| module.exports._logFunc = (logger, level, defaults, data, message, ...args) => { |
| let entry = {}; |
| |
| Object.keys(defaults || {}).forEach(key => { |
| if (key !== 'level') { |
| entry[key] = defaults[key]; |
| } |
| }); |
| |
| Object.keys(data || {}).forEach(key => { |
| if (key !== 'level') { |
| entry[key] = data[key]; |
| } |
| }); |
| |
| logger[level](entry, message, ...args); |
| }; |
| |
| /** |
| * Returns a bunyan-compatible logger interface. Uses either provided logger or |
| * creates a default console logger |
| * |
| * @param {Object} [options] Options object that might include 'logger' value |
| * @return {Object} bunyan compatible logger |
| */ |
| module.exports.getLogger = (options, defaults) => { |
| options = options || {}; |
| |
| let response = {}; |
| let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; |
| |
| if (!options.logger) { |
| // use vanity logger |
| levels.forEach(level => { |
| response[level] = () => false; |
| }); |
| return response; |
| } |
| |
| let logger = options.logger; |
| |
| if (options.logger === true) { |
| // create console logger |
| logger = createDefaultLogger(levels); |
| } |
| |
| levels.forEach(level => { |
| response[level] = (data, message, ...args) => { |
| module.exports._logFunc(logger, level, defaults, data, message, ...args); |
| }; |
| }); |
| |
| return response; |
| }; |
| |
| /** |
| * Wrapper for creating a callback than either resolves or rejects a promise |
| * based on input |
| * |
| * @param {Function} resolve Function to run if callback is called |
| * @param {Function} reject Function to run if callback ends with an error |
| */ |
| module.exports.callbackPromise = (resolve, reject) => function () { |
| let args = Array.from(arguments); |
| let err = args.shift(); |
| if (err) { |
| reject(err); |
| } else { |
| resolve(...args); |
| } |
| }; |
| |
| /** |
| * Resolves a String or a Buffer value for content value. Useful if the value |
| * is a Stream or a file or an URL. If the value is a Stream, overwrites |
| * the stream object with the resolved value (you can't stream a value twice). |
| * |
| * This is useful when you want to create a plugin that needs a content value, |
| * for example the `html` or `text` value as a String or a Buffer but not as |
| * a file path or an URL. |
| * |
| * @param {Object} data An object or an Array you want to resolve an element for |
| * @param {String|Number} key Property name or an Array index |
| * @param {Function} callback Callback function with (err, value) |
| */ |
| module.exports.resolveContent = (data, key, callback) => { |
| let promise; |
| |
| if (!callback && typeof Promise === 'function') { |
| promise = new Promise((resolve, reject) => { |
| callback = module.exports.callbackPromise(resolve, reject); |
| }); |
| } |
| |
| let content = data && data[key] && data[key].content || data[key]; |
| let contentStream; |
| let encoding = (typeof data[key] === 'object' && data[key].encoding || 'utf8') |
| .toString() |
| .toLowerCase() |
| .replace(/[-_\s]/g, ''); |
| |
| if (!content) { |
| return callback(null, content); |
| } |
| |
| if (typeof content === 'object') { |
| if (typeof content.pipe === 'function') { |
| return resolveStream(content, (err, value) => { |
| if (err) { |
| return callback(err); |
| } |
| // we can't stream twice the same content, so we need |
| // to replace the stream object with the streaming result |
| data[key] = value; |
| callback(null, value); |
| }); |
| } else if (/^https?:\/\//i.test(content.path || content.href)) { |
| contentStream = fetch(content.path || content.href); |
| return resolveStream(contentStream, callback); |
| } else if (/^data:/i.test(content.path || content.href)) { |
| let parts = (content.path || content.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i); |
| if (!parts) { |
| return callback(null, new Buffer(0)); |
| } |
| return callback(null, /\bbase64$/i.test(parts[1]) ? new Buffer(parts[2], 'base64') : new Buffer(decodeURIComponent(parts[2]))); |
| } else if (content.path) { |
| return resolveStream(fs.createReadStream(content.path), callback); |
| } |
| } |
| |
| if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) { |
| content = new Buffer(data[key].content, encoding); |
| } |
| |
| // default action, return as is |
| setImmediate(() => callback(null, content)); |
| |
| return promise; |
| }; |
| |
| /** |
| * Copies properties from source objects to target objects |
| */ |
| module.exports.assign = function ( /* target, ... sources */ ) { |
| let args = Array.from(arguments); |
| let target = args.shift() || {}; |
| |
| args.forEach(source => { |
| Object.keys(source || {}).forEach(key => { |
| if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') { |
| // tls and auth are special keys that need to be enumerated separately |
| // other objects are passed as is |
| if (!target[key]) { |
| // ensure that target has this key |
| target[key] = {}; |
| } |
| Object.keys(source[key]).forEach(subKey => { |
| target[key][subKey] = source[key][subKey]; |
| }); |
| } else { |
| target[key] = source[key]; |
| } |
| }); |
| }); |
| return target; |
| }; |
| |
| module.exports.encodeXText = str => { |
| // ! 0x21 |
| // + 0x2B |
| // = 0x3D |
| // ~ 0x7E |
| if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) { |
| return str; |
| } |
| let buf = Buffer.from(str); |
| let result = ''; |
| for (let i = 0, len = buf.length; i < len; i++) { |
| let c = buf[i]; |
| if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) { |
| result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase(); |
| } else { |
| result += String.fromCharCode(c); |
| } |
| } |
| return result; |
| }; |
| |
| |
| /** |
| * Streams a stream value into a Buffer |
| * |
| * @param {Object} stream Readable stream |
| * @param {Function} callback Callback function with (err, value) |
| */ |
| function resolveStream(stream, callback) { |
| let responded = false; |
| let chunks = []; |
| let chunklen = 0; |
| |
| stream.on('error', err => { |
| if (responded) { |
| return; |
| } |
| |
| responded = true; |
| callback(err); |
| }); |
| |
| stream.on('readable', () => { |
| let chunk; |
| while ((chunk = stream.read()) !== null) { |
| chunks.push(chunk); |
| chunklen += chunk.length; |
| } |
| }); |
| |
| stream.on('end', () => { |
| if (responded) { |
| return; |
| } |
| responded = true; |
| |
| let value; |
| |
| try { |
| value = Buffer.concat(chunks, chunklen); |
| } catch (E) { |
| return callback(E); |
| } |
| callback(null, value); |
| }); |
| } |
| |
| /** |
| * Generates a bunyan-like logger that prints to console |
| * |
| * @returns {Object} Bunyan logger instance |
| */ |
| function createDefaultLogger(levels) { |
| |
| let levelMaxLen = 0; |
| let levelNames = new Map(); |
| levels.forEach(level => { |
| if (level.length > levelMaxLen) { |
| levelMaxLen = level.length; |
| } |
| }); |
| |
| levels.forEach(level => { |
| let levelName = level.toUpperCase(); |
| if (levelName.length < levelMaxLen) { |
| levelName += ' '.repeat(levelMaxLen - levelName.length); |
| } |
| levelNames.set(level, levelName); |
| }); |
| |
| let print = (level, entry, message, ...args) => { |
| let prefix = ''; |
| if (entry) { |
| if (entry.tnx === 'server') { |
| prefix = 'S: '; |
| } else if (entry.tnx === 'client') { |
| prefix = 'C: '; |
| } |
| |
| if (entry.sid) { |
| prefix = '[' + entry.sid + '] ' + prefix; |
| } |
| |
| if (entry.cid) { |
| prefix = '[#' + entry.cid + '] ' + prefix; |
| } |
| } |
| |
| message = util.format(message, ...args); |
| message.split(/\r?\n/).forEach(line => { |
| console.log('[%s] %s %s', // eslint-disable-line no-console |
| new Date().toISOString().substr(0, 19).replace(/T/, ' '), |
| levelNames.get(level), |
| prefix + line); |
| }); |
| }; |
| |
| let logger = {}; |
| levels.forEach(level => { |
| logger[level] = print.bind(null, level); |
| }); |
| |
| return logger; |
| } |