DoubleCheck/postfix-parser

View on GitHub
index.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

var logger = require('./lib/logger');

var envEmailAddr   = '<?([^>,]*)>?';
var postfixQid     = '[0-9A-F]{10,11}';     // default queue ids
var postfixQidLong = '[0-9A-Za-z]{14,16}';  // optional 'long' ids
var postfixQidAny  = postfixQidLong + '|' + postfixQid;

var regex = {
  syslog: /^([A-Za-z]{3} [0-9 ]{2} [\d:]{8}) ([^\s]+) ([^\[]+)\[([\d]+)\]: (.*)$/,
  'submission/smtpd': new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      '(client)=([^,]+), ' +
      '(sasl_method)=([^,]+), ' +
      '(sasl_username)=(.*)$'
      ),
  smtp: new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      '(to)=' + envEmailAddr + ', ' +
      '(?:(orig_to)=' + envEmailAddr + ', )?' +
      '(relay)=([^,]+), ' +
      '(?:(conn_use)=([0-9]+), )?' +
      '(delay)=([^,]+), ' +
      '(delays)=([^,]+), ' +
      '(dsn)=([^,]+), ' +
      '(status)=(.*)$'
      ),
  'smtp-defer': new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      '(?:host) ([^ ]+) ' +
      '(?:said|refused to talk to me): ' +
      '(4[0-9]{2} .*)$'
      // '(?: \\(in reply to (?:end of )?([A-Z ]+) command\\))*'
      ),
  'smtp-timeout': new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      'conversation with ([^ ]+) ' +
      'timed out ' + '(.*)$'
      ),
  'smtp-reject': new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      'host ([^ ]+) ' +
      '(?:said|refused to talk to me): (5[0-9]{2}.*)$'
      ),
  'smtp-conn-err': /^connect to ([^ ]+): (.*)$/,
  'smtp-debug': new RegExp(
      '(?:(' + postfixQidAny + '): )?' +
      '(enabling PIX workarounds|' +
        'Cannot start TLS: handshake failure|' +
        'lost connection with .*|' +
        '^SSL_connect error to .*|' +
        '^warning: .*|' +
        'conversation with |' +
        'host )'
      ),
  qmgr: new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      '(from)=' + envEmailAddr + ', ' +
      '(?:' +
        '(size)=([0-9]+), ' +
        '(nrcpt)=([0-9]+) ' +
        '|' +
        '(status)=(.*)$' +
        ')'
       ),
  'qmgr-retry': new RegExp('^(' + postfixQidAny + '): (removed)'),
  // postfix sometimes truncates the message-id, so don't require ending >
  cleanup: new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      '((?:resent-)?message-id)=<(.*?)>?$'
      ),
  pickup: new RegExp(
      '^(?:(' + postfixQidAny + '): )?' +
      '(uid)=([0-9]+) ' +
      '(from)=' + envEmailAddr
      ),
  'pickup-retry': new RegExp(
       '^warning: ' +
       '(' + postfixQidAny + '): ' +
     '(.*)$'
     ),
  error: new RegExp(
       '^(?:(' + postfixQidAny + '): )?' +
       '(to)=' + envEmailAddr + ', ' +
       '(?:(orig_to)=' + envEmailAddr + ', )?' +
       '(relay)=([^,]+), ' +
       '(delay)=([^,]+), ' +
       '(delays)=([^,]+), ' +
       '(dsn)=([^,]+), ' +
       '(status)=(.*)$'
       ),
  'error-retry': new RegExp(
       '^warning: ' +
       '(' + postfixQidAny + '): ' +
     '(.*)$'
     ),
  bounce: new RegExp(
       '^(?:(' + postfixQidAny + '): )?' +
       'sender non-delivery notification: ' +
       '(' + postfixQidAny + '$)'
     ),
  'bounce-fatal': new RegExp(
       '^fatal: ' +
       '(.*?) ' +
       '(' + postfixQidAny + '): ' +
     '(.*)$'
     ),
  local: new RegExp(
       '^(?:(' + postfixQidAny + '): )?' +
       '(to)=' + envEmailAddr + ', ' +
       '(?:(orig_to)=' + envEmailAddr + ', )?' +
       '(relay)=([^,]+), ' +
       '(delay)=([^,]+), ' +
       '(delays)=([^,]+), ' +
       '(dsn)=([^,]+), ' +
       '(status)=(sent .*)$'
       ),
  forwardedAs: new RegExp('forwarded as (' + postfixQidAny + ')\\)'),
  scache:      new RegExp('^statistics: (.*)'),
  postscreen:  new RegExp('^(.*)'),
  postsuper:   new RegExp('^(' + postfixQidAny + '): (.*)$'),
};

exports.asObject = function (line) {

  var match = line.match(regex.syslog);
  if (!match) {
    logger.error('unparsable syslog: ' + line);
    return;
  }

  var syslog = syslogAsObject(match);
  if (!/^postfix/.test(syslog.prog)) return; // not postfix, ignore

  var parsed = exports.asObjectType(syslog.prog, syslog.msg);
  if (!parsed) {
    logger.error('unparsable ' + syslog.prog + ': ' + syslog.msg);
    return;
  }

  ['date','host','prog','pid'].forEach(function (f) {
    if (!syslog[f]) return;
    parsed[f] = syslog[f];
  });

  return parsed;
};

exports.asObjectType = function (type, line) {
  if (!type || !line) {
    logger.error('missing required arg');
    return;
  }
  if ('postfix/' === type.substr(0,8)) type = type.substr(8);

  switch (type) {
    case 'qmgr':
    case 'pickup':
    case 'error':
    case 'submission/smtpd':
      return argAsObject(type, line);
    case 'smtp':
      return smtpAsObject(line);
    case 'bounce':
      return bounceAsObject(line);
  }

  var match = line.match(regex[type]);
  if (!match) return;

  switch (type) {
    case 'syslog':
      return syslogAsObject(match);
    case 'scache':
      return { statistics: match[1] };
    case 'postscreen':
      return { postscreen: match[1] };
    case 'local':
      return localAsObject(match);
    case 'postsuper':
      return { qid: match[1], msg: match[2] };
  }

  return matchAsObject(match);
};

function syslogAsObject (match) {
  return {
    date: match[1],
    host: match[2],
    prog: match[3],
    pid:  match[4],
    msg:  match[5],
  };
}

function matchAsObject (match) {
  match.shift();
  var obj = {};
  var qid = match.shift();
  if (qid) obj.qid = qid;
  while (match.length) {
    var key = match.shift();
    var val = match.shift();
    if (key === undefined) continue;
    if (val === undefined) continue;
    obj[key] = val;
  }
  return obj;
}

function argAsObject (thing, line) {
  var match = line.match(regex[thing]);
  if (match) return matchAsObject(match);

  match = line.match(regex[thing + '-retry']);
  if (match) return { qid: match[1], msg: match[2] };
}

function smtpAsObject (line) {
  var match = line.match(regex.smtp);
  if (match) return matchAsObject(match);

  match = line.match(regex['smtp-conn-err']);
  if (match) {
    return {
      action: 'delivery',
      mx: match[1],
      err: match[2]
    };
  }

  match = line.match(regex['smtp-defer']);
  if (match) {
    return {
      action: 'defer',
      qid: match[1],
      host: match[2],
      msg: match[3]
    };
  }

  match = line.match(regex['smtp-reject']);
  if (match) {
    return {
      action: 'reject',
      qid: match[1],
      host: match[2],
      msg: match[3],
    };
  }

  match = line.match(regex['smtp-timeout']);
  if (match) {
    return {
      action: 'defer',
      qid: match[1],
      host: match[2],
      msg: match[3],
    };
  }

  match = line.match(regex['smtp-debug']);
  if (!match) return;
  if (match[1] && match[2]) {
    return {
      qid: match[1],
      msg: match[2],
    };
  }
  return { msg: match[0] };
}

function bounceAsObject (line) {

  var match = line.match(regex.bounce);
  if (match) {
    match.shift();
    var obj = {};
    var qid = match.shift();
    if (qid) obj.qid = qid;
    obj.dsnQid = match.shift();
    return obj;
  }

  match = line.match(regex['bounce-fatal']);
  if (match) {
    return {
      qid: match[2],
      msg: 'fatal: ' + match[1] + ': ' + match[3]
    };
  }
}

function localAsObject(match) {
  var obj = matchAsObject(match);
  var m = obj.status.match(regex.forwardedAs);
  if (m) {
    obj.status = 'forwarded';
    obj.forwardedAs = m[1];
  }
  return obj;
}