index.js
// accounting_files
//------------
// documentation via: `haraka -h accounting_files`
//var outbound = require("./outbound");
var fs = require("fs");
var path = require("path");
var dateFormat = require('dateformat');
var cfg;
exports.register = function () {
var plugin = this;
plugin.load_accounting_file_ini();
plugin.register_hook('init_master', 'init_plugin');
plugin.register_hook('init_child', 'init_plugin');
plugin.register_hook('queue_outbound', 'set_header_to_note');
plugin.register_hook('delivered', 'delivered');
plugin.register_hook('deferred', 'deferred');
plugin.register_hook('bounce', 'bounce');
};
//-------------------------------------------------------------------------------------------------------------------
//Plugin Hooks ------------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
//Init plugin
exports.init_plugin = function (next) {
var context = this;
var acct_path = cfg.main.path || path.join(process.env.HARAKA, "accounting_files");
var separator = cfg.main.separator || " ";
var files_extension = cfg.main.extension || "tsv";
var archive_interval_val = cfg.main.archive_interval || 86400;
var default_archive_to_dir = "archive";
var max_size = cfg.main.max_size || 200;
//Setting global variables to notes
server.notes.acct_path = acct_path;
server.notes.separator = separator;
server.notes.files_extension = files_extension;
server.notes.max_size = max_size;
//Accounting files directories
if ( cfg.hasOwnProperty("location") ) {
server.notes.delivered_dir_path = path.join(acct_path, ( cfg.location.delivered || "delivered" ));
server.notes.deferred_dir_path = path.join(acct_path, ( cfg.location.deferred || "deferred" ));
server.notes.bounce_dir_path = path.join(acct_path, ( cfg.location.bounce || "bounce" ));
} else {
server.notes.delivered_dir_path = path.join(acct_path, "delivered");
server.notes.deferred_dir_path = path.join(acct_path, "deferred");
server.notes.bounce_dir_path = path.join(acct_path, "bounce");
}
//Accounting files fields
if ( cfg.hasOwnProperty("fields") ) {
server.notes.delivered_fields = ( cfg.fields.delivered || "type,timeLogged,timeQueued,rcpt,srcMta,srcIp,vmta,jobId,dsnStatus,dsnMsg" ).split(",");
server.notes.deferred_fields = ( cfg.fields.deferred || "type,timeLogged,timeQueued,rcpt,srcMta,srcIp,vmta,jobId,dsnStatus,dsnMsg,delay" ).split(",");
server.notes.bounce_fields = ( cfg.fields.bounce || "type,timeLogged,timeQueued,rcpt,srcMta,srcIp,vmta,jobId,dsnStatus,dsnMsg,bounceCat" ).split(",");
} else {
server.notes.delivered_fields = ["type","timeLogged","timeQueued","rcpt","srcMta","srcIp","vmta","jobId","dsnStatus","dsnMsg"];
server.notes.deferred_fields = ["type","timeLogged","timeQueued","rcpt","srcMta","srcIp","vmta","jobId","dsnStatus","dsnMsg","delay"];
server.notes.bounce_fields = ["type","timeLogged","timeQueued","rcpt","srcMta","srcIp","vmta","jobId","dsnStatus","dsnMsg","bounceCat"];
}
//-------------------------------------------------------------------------------------------------------
//Init plugin directories
createDirectoryIfNotExist(acct_path); //Create main directory
createDirectoryIfNotExist(server.notes.delivered_dir_path); //Create delivered directory
createDirectoryIfNotExist(server.notes.deferred_dir_path); //Create deferred directory
createDirectoryIfNotExist(server.notes.bounce_dir_path); //Create bounce directory
//Init plugin files with the header fields
GenerateNewFile('delivered');
GenerateNewFile('deferred');
GenerateNewFile('bounce');
//-------------------------------------------------------------------------------------------------------
//Accounting files "archive_to" directories if "archiving" option is enabled
if ( cfg.main.hasOwnProperty("archiving") ) {
if ( cfg.main.archiving === "true") {
var delivered_archive_to_dir_name = default_archive_to_dir;
var deferred_archive_to_dir_name = default_archive_to_dir;
var bounce_archive_to_dir_name = default_archive_to_dir;
if ( cfg.hasOwnProperty("archive_to") ) {
delivered_archive_to_dir_name = cfg.archive_to.delivered || default_archive_to_dir;
deferred_archive_to_dir_name = cfg.archive_to.deferred || default_archive_to_dir;
bounce_archive_to_dir_name = cfg.archive_to.bounce || default_archive_to_dir;
}
}
server.notes.delivered_archive_to_dir_path = path.join( server.notes.delivered_dir_path, delivered_archive_to_dir_name);
server.notes.deferred_archive_to_dir_path = path.join( server.notes.deferred_dir_path, deferred_archive_to_dir_name);
server.notes.bounce_archive_to_dir_path = path.join( server.notes.bounce_dir_path, bounce_archive_to_dir_name);
if ( cfg.main.archiving === "true") {
//Init plugin 'archiving' directories
createDirectoryIfNotExist( server.notes.delivered_archive_to_dir_path ); //Create delivered archiving to directory
createDirectoryIfNotExist( server.notes.deferred_archive_to_dir_path ); //Create deferred archiving to directory
createDirectoryIfNotExist( server.notes.bounce_archive_to_dir_path ); //Create bounce archiving to directory
//Set archiving interval if "archive_to" is specified in config files
server.notes.archive_interval = setInterval( function () {
archivingDirFiles( server.notes.delivered_dir_path, server.notes.delivered_archive_to_dir_path, [delivered_archive_to_dir_name], context); //Archiving delivered files
archivingDirFiles( server.notes.deferred_dir_path, server.notes.deferred_archive_to_dir_path, [deferred_archive_to_dir_name], context); //Archiving deferred files
archivingDirFiles( server.notes.bounce_dir_path, server.notes.bounce_archive_to_dir_path, [bounce_archive_to_dir_name], context); //Archiving bounce files
}, archive_interval_val * 1000 );
}
}
context.loginfo("Plugin is Ready!");
return next();
};
exports.set_header_to_note = function (next, connection) {
//Setting the header to notes before sent, we will need it in 'delivered/bounce/deferred' hooks to get "custom_FIELD" parameters
connection.transaction.notes.header = connection.transaction.header;
return next();
};
exports.delivered = function (next, hmail, params) {
var plugin = this;
var todo = hmail.todo;
var header = hmail.notes.header;
var rcpt_to = todo.rcpt_to[0];
if (!todo) return next();
var fields_values = {};
server.notes.delivered_fields.forEach ( function (field) {
switch (field) {
case "type" :
fields_values.type = "d";
break;
case "timeLogged" :
fields_values.timeLogged = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss");
break;
case "timeQueued" :
fields_values.timeQueued = dateFormat(new Date(todo.queue_time), "yyyy-mm-dd HH:MM:ss");
break;
case "rcpt" :
fields_values.rcpt = rcpt_to.original.slice(1, -1);
break;
case "srcType" :
fields_values.srcType = "smtp";
break;
case "srcMta" :
fields_values.srcMta = todo.notes.outbound_helo;
break;
case "srcIp" :
fields_values.srcIp = todo.notes.outbound_ip;
break;
case "destIp" :
fields_values.destIp = params[1] || " - ";
break;
case "vmta" :
fields_values.vmta = server.notes.vmta || " - ";
break;
case "jobId" :
fields_values.jobId = todo.uuid;
break;
case (field.match(/^custom_/) || {}).input :
fields_values[field] = header.get(field) || " - ";
break;
case "dsnStatus" :
fields_values.dsnStatus = " - ";
break;
case "dsnMsg" :
fields_values.dsnMsg = " - ";
break;
case "delay" :
fields_values.delay = " - ";
break;
}
});
addRecord(server.notes.delivered_file_path, server.notes.delivered_fields, fields_values, 'delivered', this);
plugin.loginfo("Delivered Record Added.");
return next();
};
exports.deferred = function (next, hmail, params) {
var plugin = this;
var todo = hmail.todo;
var header = hmail.notes.header;
var rcpt_to = todo.rcpt_to[0];
if (!todo) return next();
var fields_values = {};
server.notes.deferred_fields.forEach ( function (field) {
switch (field) {
case "type" :
fields_values.type = "df";
break;
case "timeLogged" :
fields_values.timeLogged = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss");
break;
case "timeQueued" :
fields_values.timeQueued = dateFormat(new Date(todo.queue_time), "yyyy-mm-dd HH:MM:ss");
break;
case "rcpt" :
fields_values.rcpt = rcpt_to.original.slice(1, -1);
break;
case "srcType" :
fields_values.srcType = "smtp";
break;
case "srcMta" :
fields_values.srcMta = todo.notes.outbound_helo;
break;
case "srcIp" :
fields_values.srcIp = todo.notes.outbound_ip;
break;
case "destIp" :
fields_values.destIp = " - ";
break;
case "vmta" :
fields_values.vmta = server.notes.vmta || " - ";
break;
case "jobId" :
fields_values.jobId = todo.uuid;
break;
case (field.match(/^custom_/) || {}).input :
fields_values[field] = header.get(field) || " - ";
break;
case "dsnStatus" :
fields_values.dsnStatus = rcpt_to.dsn_code || rcpt_to.dsn_status;
break;
case "dsnMsg" :
fields_values.dsnMsg = rcpt_to.dsn_smtp_response;
break;
case "bounceCat" :
fields_values.bounceCat = " - ";
break;
case "delay" :
fields_values.delay = params.delay;
break;
}
});
addRecord(server.notes.deferred_file_path, server.notes.deferred_fields, fields_values, 'deferred', this);
plugin.loginfo("Deferred Record Added.");
return next();
};
exports.bounce = function (next, hmail, error) {
var plugin = this;
var todo = hmail.todo;
var header = hmail.notes.header;
var rcpt_to = todo.rcpt_to[0];
if (!todo) return next();
var fields_values = {};
server.notes.bounce_fields.forEach ( function (field) {
switch (field) {
case "type" :
fields_values.type = "b";
break;
case "timeLogged" :
fields_values.timeLogged = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss");
break;
case "timeQueued" :
fields_values.timeQueued = dateFormat(new Date(todo.queue_time), "yyyy-mm-dd HH:MM:ss");
break;
case "rcpt" :
fields_values.rcpt = rcpt_to.original.slice(1, -1);
break;
case "srcType" :
fields_values.srcType = "smtp";
break;
case "srcMta" :
fields_values.srcMta = todo.notes.outbound_helo;
break;
case "srcIp" :
fields_values.srcIp = todo.notes.outbound_ip;
break;
case "destIp" :
fields_values.destIp = "destIp";
break;
case "vmta" :
fields_values.vmta = server.notes.vmta || " - ";
break;
case "jobId" :
fields_values.jobId = todo.uuid;
break;
case (field.match(/^custom_/) || {}).input :
fields_values[field] = header.get(field) || " - ";
break;
case "dsnStatus" :
fields_values.dsnStatus = rcpt_to.dsn_code || rcpt_to.dsn_status;
break;
case "dsnMsg" :
if ( rcpt_to.hasOwnProperty("dsn_code"))
fields_values.dsnMsg = rcpt_to.reason || (rcpt_to.dsn_code + " " + rcpt_to.dsn_msg);
else if ( rcpt_to.hasOwnProperty("dsn_smtp_code"))
fields_values.dsnMsg = rcpt_to.dsn_smtp_code + " " + rcpt_to.dsn_status + " " + rcpt_to.dsn_smtp_response;
else
fields_values.dsnMsg = " - ";
break;
case "bounceCat" :
fields_values.bounceCat = rcpt_to.reason || (rcpt_to.dsn_code + " (" + rcpt_to.dsn_msg + ")");
break;
case "delay" :
fields_values.delay = " - ";
break;
}
});
addRecord(server.notes.bounce_file_path, server.notes.bounce_fields, fields_values, 'bounce', this);
plugin.loginfo("Bounce Record Added.");
//Prevent the sending of bounce mail to originating sender
return next(OK);
};
exports.shutdown = function () {
//clear the "archive_interval" interval if "archive_to" is specified in the config files
if ( cfg.main.hasOwnProperty("archiving") ) {
if ( cfg.main.archiving === "true") {
clearInterval(server.notes.archive_interval);
}
}
};
//-------------------------------------------------------------------------------------------------------------------
//Plugin Functions --------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------------------------
//Load configuration file
exports.load_accounting_file_ini = function () {
var plugin = this;
plugin.loginfo("Accounting_file configs are fully loaded from 'accounting_files.ini'.");
cfg = plugin.config.get("accounting_files.ini", function () {
plugin.register();
});
plugin.loginfo(cfg);
};
//Generate new files for the 3 types 'delivered/deferred/bounce'
var GenerateNewFile = function (type){
if ( type == 'delivered' ){
//Set new paths to the notes
server.notes.delivered_file_path = path.join( server.notes.delivered_dir_path, "d." + dateFormat(new Date(), "yyyy-mm-dd-HHMMss") + "." + server.notes.files_extension);
createFileIfNotExist(server.notes.delivered_file_path, server.notes.delivered_fields); //Create delivered file
//Return new files names
return path.basename(server.notes.delivered_file_path);
}
else if ( type == 'deferred' ){
//Set new paths to the notes
server.notes.deferred_file_path = path.join( server.notes.deferred_dir_path, "t." + dateFormat(new Date(), "yyyy-mm-dd-HHMMss") + "." + server.notes.files_extension);
createFileIfNotExist(server.notes.deferred_file_path, server.notes.deferred_fields); //Create deferred file
//Return new files names
return path.basename(server.notes.deferred_file_path);
}
else if ( type == 'bounce' ){
//Set new paths to the notes
server.notes.bounce_file_path = path.join( server.notes.bounce_dir_path, "b." + dateFormat(new Date(), "yyyy-mm-dd-HHMMss") + "." + server.notes.files_extension);
createFileIfNotExist(server.notes.bounce_file_path, server.notes.bounce_fields); //Create bounce file
//Return new files names
return path.basename(server.notes.bounce_file_path);
}
};
//Create directory if not exist
var createDirectoryIfNotExist = function (dir_name) {
if ( !fs.existsSync(dir_name) ) {
fs.mkdirSync(dir_name);
}
};
//Create file if not exist and add the file header from the passed fields
var createFileIfNotExist = function (filename, fields) {
if ( !fs.existsSync(filename) ) {
fs.writeFileSync(filename);
//Set file header from the pre-defined fields
setHeaderFromFields(filename, fields);
}
};
//Set Header to file from fields
var setHeaderFromFields = function (filename, fields) {
var headers = "";
fields.forEach ( function (field) {
headers += field + server.notes.separator;
});
fs.writeFileSync(filename, headers + "\r\n");
};
//Add new record to the passed accounting file in parameters
var addRecord = function (filename, fields, fields_values, type, context) {
var separator = server.notes.separator;
var record = "";
fields.forEach ( function (field) {
record += fields_values[field] + separator;
});
fs.appendFileSync(filename, record + "\r\n");
checkSizeAndMove(filename, type, context);
};
//Move all the content of 'from_dir' directory to 'to_dir' directory except the 'files/directories' passed in the 'except' array
var archivingDirFiles = function (from_dir, to_dir, except, context) {
//Generate new files and switch to them before the archiving so the logging of the data will not stop
var new_files = [];
new_files.push( GenerateNewFile('delivered', context) );
new_files.push( GenerateNewFile('deferred', context) );
new_files.push( GenerateNewFile('bounce', context) );
//Add new files names 'except' array to exclude them from the archiving
except = except.concat(new_files);
//archiving content
fs.readdir(from_dir, function (err, files) {
files.forEach ( function (filename) {
if (except.indexOf(filename) == -1) {
fs.rename(from_dir + "/" + filename, to_dir + "/" + filename, function (_err) {
if (_err) {
context.loginfo("Can't archive file '" + filename + "' " + _err);
throw _err;
} else {
context.loginfo("File '" + filename + "' Archived");
}
});
}
});
});
};
//Get the size of the passed filename in megabyte
var getFileSizeInMegabyte = function ( filename ) {
var stats = fs.statSync(filename);
var fileSizeInBytes = stats.size;
return fileSizeInBytes / 1000000.0;
};
//Check the size of the file if it's greater or equals to 'max_size' archive the file to the related directory and generate a new file
var checkSizeAndMove = function ( filename, type, context ) {
var file_size = getFileSizeInMegabyte(filename);
if ( file_size >= server.notes.max_size ) {
var archive_to_path = '';
var file_base_name = path.basename(filename);
if ( type == 'delivered' )
archive_to_path = server.notes.delivered_archive_to_dir_path;
else if ( type == 'deferred' )
archive_to_path = server.notes.deferred_archive_to_dir_path;
else if ( type == 'bounce' )
archive_to_path = server.notes.bounce_archive_to_dir_path;
//Generate and switch to new file that will replace the moved one
GenerateNewFile(type, context);
fs.rename(filename, archive_to_path + "/" + file_base_name, function (err) {
if (err) {
context.loginfo("Can't move file '" + file_base_name + "' " + err);
throw err;
} else {
context.loginfo("File '" + file_base_name + "' moved");
}
});
}
};