haraka/haraka-plugin-dcc

View on GitHub
index.js

Summary

Maintainability
A
3 hrs
Test Coverage
// dcc client
// http://www.dcc-servers.net/dcc/dcc-tree/dccifd.html

const net = require('net');

exports.register = function () {
    this.load_dcc_ini();
}

exports.load_dcc_ini = function () {
    const plugin = this;
    plugin.cfg = plugin.config.get('dcc.ini', () => {
        plugin.load_dcc_ini();
    });
}

exports.get_host = function (host) {
    switch (host) {
        case 'Unknown':
        case 'NXDOMAIN':
        case 'DNSERROR':
        case undefined:
            return undefined;
        default:
            return host;
    }
}

exports.should_train = function (txn) {
    if (txn.notes.training_mode && txn.notes.training_mode === 'spam')
        return ' spam';
    return '';
}

exports.human_result = function (code) {
    switch (code) {
        case 'A': return 'Accept';
        case 'G': return 'Greylist';
        case 'R': return 'Reject';
        case 'S': return 'Accept some';
        case 'T': return 'Temp fail';
    }
}

exports.get_result = function (c, result) {
    const plugin = this;

    // Get result code
    switch (result) {
        case 'A':
            // Accept, fall through
        case 'G':
            // Greylist, fall through
        case 'R':
            // Reject, fall through
        case 'S':
            // Accept for some recipients, fall through
        case 'T':
            // Temporary failure
            break;
        default:
            c.logerror(plugin, 'invalid result: ' + result);
            break;
    }
    return result;
}

exports.human_disposition = function (code) {
    switch (code) {
        case 'A': return 'Accept';
        case 'G': return 'Greylist/Discard';
        case 'R': return 'Reject';
    }
}

exports.get_disposition = function (c, disposition) {
    const plugin = this;

    switch (disposition) {
        case 'A':    // Deliver the message
        case 'G':    // Discard the message during greylist embargo
        case 'R':    // Discard the message as spam
            break;
        default:
            c.logerror(plugin, 'invalid disposition: ' + disposition);
            break;
    }

    return disposition;
}

exports.get_request_headers = function (conn, training) {
    const plugin = this;
    const txn = conn.transaction;
    const host  = plugin.get_host(conn.remote.host);

    const headers = [
        'header' + training,
        conn.remote.ip + ((host) ? '\r' + host : ''),
        conn.hello.host,
        txn.mail_from.address(),
        txn.rcpt_to.map((rcpt) => { return rcpt.address(); }).join('\r'),
    ].join('\n');

    conn.logdebug(plugin, 'sending protocol headers: ' + headers);
    return headers + '\n\n';
}

exports.get_response_headers = function (c, rl) {
    // Read headers
    const headers = [];
    for (let i=0; i<rl.length; i++) {
        if (/^\s/.test(rl[i]) && headers.length) {
            // Continuation
            headers[headers.length-1] += rl[i];
        }
        else {
            if (rl[i]) headers.push(rl[i]);
        }
    }
    c.logdebug(this, 'found ' + headers.length + ' headers');

    for (let h=0; h<headers.length; h++) {
        const header = headers[h].toString('utf8').trim();
        let match;
        if ((match = /^([^: ]+):\s*((?:.|[\r\n])+)/.exec(header))) {
            c.transaction.add_header(match[1], match[2]);
        }
        else {
            c.logerror(this, 'header did not match regexp: ' + header);
        }
    }

    return headers;
}

exports.hook_data_post = function (next, connection) {
    const plugin = this;

    // Fix-up rDNS for DCC
    const training = plugin.should_train(connection.transaction);
    let response = '';
    let client;

    function onConnect () {
        connection.logdebug(plugin, 'connected to dcc');

        this.write(plugin.get_request_headers(connection, training) , () => {
            connection.transaction.message_stream.pipe(client);
        });
    }

    const c = plugin.cfg.dccifd;
    if (c.path) {
        client = net.createConnection(c.path, onConnect);
    }
    else {
        client = net.createConnection(c.port, c.host, onConnect);
    }

    client
        .on('error', function (err) {
            connection.logerror(plugin, err.message);
            return next();
        })
        .on('data', function (chunk) {
            response += chunk.toString('utf8');
        })
        .on('end', function () {
            const rl = response.split("\n");
            if (rl.length < 2) {
                connection.logwarn(plugin, 'invalid response: ' + response + 'length=' + rl.length);
                return next();
            }
            connection.logdebug(plugin, 'got response: ' + response);

            const result      = plugin.get_result(connection, rl.shift());
            const disposition = plugin.get_disposition(connection, rl.shift());
            const headers     = plugin.get_response_headers(connection, rl);

            connection.transaction.results.add(plugin, {
                'training': (training ? true : false),
                'result': plugin.human_result(result),
                'disposition': plugin.human_disposition(disposition),
                headers,
            });

            connection.loginfo(plugin, 'training=' + (training ? 'Y' : 'N') +
                   ` result=${result} disposition=${disposition} headers=${headers.length}`);

            return next();
        });
}