haraka/haraka-utils

View on GitHub
index.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict';

const child = require('child_process');
const fs = require('fs');
const path = require('path');

// copied from http://www.broofa.com/Tools/Math.uuid.js
const CHARS =
  '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');

exports.uuid = function () {
  const chars = CHARS;
  const uuid = new Array(36);
  let rnd = 0;
  let r;
  for (let i = 0; i < 36; i++) {
    if (i === 8 || i === 13 || i === 18 || i === 23) {
      uuid[i] = '-';
    } else if (i === 14) {
      uuid[i] = '4';
    } else {
      if (rnd <= 0x02) rnd = (0x2000000 + Math.random() * 0x1000000) | 0;
      r = rnd & 0xf;
      rnd = rnd >> 4;
      uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r];
    }
  }
  return uuid.join('');
};

exports.in_array = function (item, array) {
  if (!array) return false;
  if (!Array.isArray(array)) return false;
  return array.includes(item);
};

exports.to_object = function (array) {
  if (typeof array === 'string') {
    array = array.split(/[\s,;]+/);
  }
  if (!Array.isArray(array)) {
    throw 'arguments to to_object must be a string or array';
  }
  const rv = {};
  for (let i = 0; i < array.length; i++) {
    if (array[i] === undefined) {
      continue;
    }
    rv[array[i]] = true;
  }
  return rv;
};

exports.sort_keys = function (obj) {
  return Object.keys(obj).sort();
};

exports.uniq = function (arr) {
  const out = [];
  for (const i of arr) {
    if (out.includes(i)) continue;
    out.push(i);
  }
  return out;
};

exports.extend = function (target) {
  // http://stackoverflow.com/questions/14974864/
  const sources = [].slice.call(arguments, 1);
  sources.forEach(function (source) {
    for (const prop in source) {
      target[prop] = source[prop];
    }
  });
  return target;
};

exports.ISODate = function (d) {
  function pad(n) {
    return n < 10 ? `0${n}` : n;
  }
  return `${d.getUTCFullYear()}-\
${pad(d.getUTCMonth() + 1)}-\
${pad(d.getUTCDate())}T\
${pad(d.getUTCHours())}:\
${pad(d.getUTCMinutes())}:\
${pad(d.getUTCSeconds())}Z`;
};

const _daynames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const _monnames = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec',
];

function _pad(num, n, p) {
  return num.toString().padStart(n, p || '0');
}

exports.pad = _pad;

exports.date_to_str = function (d) {
  // https://www.rfc-editor.org/rfc/rfc2822#section-3.3
  return `${_daynames[d.getDay()]}, ${_pad(d.getDate(), 2)} \
${_monnames[d.getMonth()]} ${d.getFullYear()} \
${_pad(d.getHours(), 2)}:${_pad(d.getMinutes(), 2)}:\
${_pad(d.getSeconds(), 2)} \
${d.toString().match(/\sGMT([+-]\d+)/)[1]}`;
};

exports.decode_qp = function (line) {
  console.warn(`SUNSET: 2025`);
  line = line.replace(/\r\n/g, '\n').replace(/[ \t]+\r?\n/g, '\n');
  if (!/=/.test(line)) return Buffer.from(line); // maybe a pointless optimisation

  line = line.replace(/=\n/gm, '');
  const buf = Buffer.alloc(line.length);
  let pos = 0;
  for (let i = 0, l = line.length; i < l; i++) {
    if (
      line[i] === '=' &&
      /=[0-9a-fA-F]{2}/.test(`${line[i]}${line[i + 1]}${line[i + 2]}`)
    ) {
      i++;
      buf[pos] = parseInt(`${line[i]}${line[i + 1]}`, 16);
      i++;
    } else {
      buf[pos] = line.charCodeAt(i);
    }
    pos++;
  }
  return buf.slice(0, pos);
};

function _char_to_qp(ch) {
  console.warn(`SUNSET: 2025`);
  return _buf_to_qp(Buffer.from(ch));
}

function _is_printable(charCode) {
  console.warn(`SUNSET: 2025`);
  switch (charCode) {
    case 61: // = (special in encoded words)
      return false;
    case 13: // CR
    case 10: // LF
      return true;
  }
  // much faster than a compound switch
  if (charCode > 32 && charCode <= 126) return true;
  return false;
}

function _buf_to_qp(b) {
  console.warn(`SUNSET: 2025`);
  let r = '';
  for (let i = 0; i < b.length; i++) {
    if (_is_printable(b[i])) {
      r = `${r}${String.fromCharCode(b[i])}`;
    } else {
      r = `${r}=${_pad(b[i].toString(16).toUpperCase(), 2)}`;
    }
  }
  return r;
}

// Shameless attempt to copy from Perl's MIME::QuotedPrint::Perl code.
const qpRe = /([^ \t\n!"#$%&'()*+,\-./0-9:;<>?@A-Z[\\\]^_`a-z{|}~])/g;
function asQuotedPrintable(str) {
  console.warn(`SUNSET: 2025`);
  if (Buffer.isBuffer(str)) return _buf_to_qp(str);

  return str
    .replace(qpRe, (orig, p1) => {
      return _char_to_qp(p1);
    })
    .replace(/([ \t]+)$/gm, (orig, p1) => {
      return p1.split('').map(_char_to_qp).join('');
    });
}

// NOTE: deprecated. Haraka now uses 'libqp' instead.
// See https://github.com/haraka/haraka-utils/issues/22
exports.encode_qp = (str) => {
  console.warn(`SUNSET: 2025`);
  // https://tools.ietf.org/html/rfc2045#section-6.7
  str = asQuotedPrintable(str);

  // Shorten lines to 76 chars, but don't break =XX encodes.
  // Method: iterate over to char 73.
  //   If char 74, 75 or 76 is = we need to break before the =.
  //   Otherwise break at 76.
  let cur_length = 0;
  let out = '';
  for (let i = 0; i < str.length; i++) {
    if (str[i] === '\n') {
      out = `${out}\n`;
      cur_length = 0;
      continue;
    }

    cur_length++;
    if (cur_length <= 73) {
      out = `${out}${str[i]}`;
    } else if (cur_length > 73 && cur_length < 76) {
      if (str[i] === '=') {
        out = `${out}=\n=`;
        cur_length = 1;
      } else {
        out = `${out}${str[i]}`;
      }
    } else {
      // Otherwise got to char 76

      // Don't insert '=\n' if end of string or next char is already \n:
      if (i === str.length - 1 || str[i + 1] === '\n') {
        out = `${out}${str[i]}`;
      } else {
        out = `${out}=\n${str[i]}`;
        cur_length = 1;
      }
    }
  }

  return out;
};

exports.node_min = function (min, cur) {
  const wants = min.split('.');
  const has = (cur || process.version.substring(1)).split('.');

  for (let i = 0; i <= 3; i++) {
    // note use of unary + for fast type conversion to num
    if (+has[i] > +wants[i]) return true;
    if (+has[i] < +wants[i]) return false;
  }

  // they're identical
  return true;
};

exports.existsSync = require(
  exports.node_min('0.8') ? 'fs' : 'path',
).existsSync;

exports.indexOfLF = function (buf, maxlength) {
  for (let i = 0; i < buf.length; i++) {
    if (maxlength && i === maxlength) break;
    if (buf[i] === 0x0a) return i;
  }
  return -1;
};

exports.prettySize = function (size) {
  if (size === 0 || !size) return 0;
  const i = Math.floor(Math.log(size) / Math.log(1024));
  // https://wikipedia.org/wiki/Binary_prefix units with 1024 base
  // should use binary prefix
  const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
  return `${(size / Math.pow(1024, i)).toFixed(2) * 1}${units[i]}`;
};

exports.valid_regexes = function (list, file) {
  // list: an array of regexes. file: the file name containing the regex list
  const valid = [];
  for (let i = 0; i < list.length; i++) {
    try {
      new RegExp(list[i]);
    } catch (e) {
      console.error(`invalid regex in ${file}, ${list[i]}`);
      continue;
    }
    valid.push(list[i]);
  }
  return valid; // returns a list of valid regexes
};

exports.regexp_escape = function (text) {
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
};

exports.base64 = function (str) {
  return Buffer.from(str, 'UTF-8').toString('base64');
};

exports.unbase64 = function (str) {
  return Buffer.from(str, 'base64').toString('UTF-8');
};

// Fisher-Yates shuffle
// http://bost.ocks.org/mike/shuffle/
exports.shuffle = function (array) {
  let m = array.length;
  let t;
  let i;

  // While there remain elements to shuffle…
  while (m) {
    // Pick a remaining element…
    i = Math.floor(Math.random() * m--);

    // And swap it with the current element.
    t = array[m];
    array[m] = array[i];
    array[i] = t;
  }

  return array;
};

exports.elapsed = function (start, decimal_places) {
  const diff = (Date.now() - start) / 1000; // in seconds

  if (decimal_places === undefined) {
    decimal_places = diff > 5 ? 0 : diff > 2 ? 1 : 2;
  } else {
    decimal_places = parseInt(decimal_places);
    if (isNaN(decimal_places)) {
      decimal_places = 2;
    }
  }
  return diff.toFixed(decimal_places);
};

exports.wildcard_to_regexp = function (str) {
  return `${str
    .replace(/[-[\]/{}()*+?.,\\^$|#\s]/g, '\\$&')
    .replace('\\*', '.*')
    .replace('\\?', '.')}$`;
};

exports.line_regexp = /^([^\n]*\n)/;

exports.copyDir = function (srcPath, dstPath) {
  exports.mkDir(dstPath);

  for (const file of fs.readdirSync(srcPath)) {
    // Ignore ".*"
    if (/^\./.test(file)) continue;

    const srcFile = path.join(srcPath, file);
    const dstFile = path.join(dstPath, file);

    const srcStat = fs.statSync(srcFile);

    if (srcStat.isDirectory()) {
      // if directory
      exports.copyDir(srcFile, dstFile); // recurse
    } else if (srcStat.isFile()) {
      // if file
      exports.copyFile(srcFile, dstFile); // copy to dstPath (no overwrite)
    }
  }
};

exports.copyFile = function (srcFile, dstFile) {
  try {
    if (fs.statSync(dstFile).isFile()) {
      warningMsg(`EEXIST, File exists '${dstFile}'`);
      return;
    }
    throw `EEXIST but not a file: '${dstFile}'`;
  } catch (e) {
    // File NOT exists
    if (e.code == 'ENOENT') {
      exports.mkDir(path.dirname(dstFile));
      fs.writeFileSync(dstFile, fs.readFileSync(srcFile));
      createMsg(dstFile);
    } else {
      console.log(`copy ${srcFile} to ${dstFile}`);
      throw e;
    }
  }
};

exports.createFile = function (filePath, data, info = {}, force = false) {
  try {
    if (fs.existsSync(filePath) && !force) {
      throw `${filePath} already exists`;
    }
    exports.mkDir(path.dirname(filePath));
    const fd = fs.openSync(filePath, 'w');
    const output = data.replace(/%(\w+)%/g, function (i, m1) {
      return info[m1];
    });
    fs.writeSync(fd, output, null);
  } catch (e) {
    warningMsg(`Unable to create file: ${e}`);
  }
};

function createMsg(dirPath) {
  console.log(`\x1b[32mcreate\x1b[0m: ${dirPath}`);
}

function warningMsg(msg) {
  console.error(`\x1b[31mwarning\x1b[0m: ${msg}`);
}

exports.mkDir = function (dstPath) {
  try {
    if (fs.statSync(dstPath).isDirectory()) return;
  } catch (ignore) {}

  try {
    fs.mkdirSync(dstPath, { recursive: true });
    createMsg(dstPath);
  } catch (e) {
    // File exists
    console.error(e);
    if (e.errno == 17) {
      warningMsg(e.message);
    } else {
      throw e;
    }
  }
};

exports.getGitCommitId = (dir) => {
  return child
    .spawnSync('git', ['-C', dir, 'show', '--format="%h"', '--no-patch'])
    .stdout.toString()
    .replaceAll('"', '')
    .trim();
};

exports.getVersion = function (pkgDir) {
  if (this._version) return this._version; // cache

  const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json')));
  this._version = pkg.version;

  try {
    // if within a git repo
    fs.statSync(path.join(pkgDir, '.git'));
    const commitId = this.getGitCommitId(pkgDir);
    if (commitId) this._version += `/${commitId}`;
  } catch (ignore) {}

  return this._version;
};