'use strict';

// Simple LMTP server that accepts all messages for valid recipients

const config = require('wild-config');
const log = require('npmlog');
const ObjectID = require('mongodb').ObjectID;
const SMTPServer = require('smtp-server').SMTPServer;
const tools = require('./lib/tools');
const MessageHandler = require('./lib/message-handler');
const UserHandler = require('./lib/user-handler');
const FilterHandler = require('./lib/filter-handler');
const db = require('./lib/db');
const certs = require('./lib/certs');
const Gelf = require('gelf');
const os = require('os');

let messageHandler;
let userHandler;
let filterHandler;
let loggelf;

config.on('reload', () => {
    log.info('LMTP', 'Configuration reloaded');
});

const serverOptions = {
    lmtp: true,

    secure: config.lmtp.secure,
    secured: config.lmtp.secured,

    // log to console
    logger: {
        info(...args) {
            args.shift();
            log.info('LMTP', ...args);
        },
        debug(...args) {
            args.shift();
            log.silly('LMTP', ...args);
        },
        error(...args) {
            args.shift();
            log.error('LMTP', ...args);
        }
    },

    name: config.lmtp.name || false,

    // not required but nice-to-have
    banner: config.lmtp.banner || 'Welcome to WildDuck Mail Server',

    disabledCommands: ['AUTH'].concat(config.lmtp.disableSTARTTLS ? 'STARTTLS' : []),

    onMailFrom(address, session, callback) {
        // reset session entries
        session.users = [];

        // accept alls sender addresses
        return callback();
    },

    // Validate RCPT TO envelope address. Example allows all addresses that do not start with 'deny'
    // If this method is not set, all addresses are allowed
    onRcptTo(rcpt, session, callback) {
        let originalRecipient = tools.normalizeAddress(rcpt.address);
        userHandler.get(
            originalRecipient,
            {
                name: true,
                forwards: true,
                targets: true,
                autoreply: true,
                encryptMessages: true,
                encryptForwarded: true,
                pubKey: true,
                spamLevel: true
            },
            (err, userData) => {
                if (err) {
                    log.error('LMTP', err);
                    return callback(new Error('Database error'));
                }
                if (!userData) {
                    return callback(new Error('Unknown recipient'));
                }

                if (!session.users) {
                    session.users = [];
                }

                session.users.push({
                    recipient: originalRecipient,
                    user: userData
                });

                callback();
            }
        );
    },

    // Handle message stream
    onData(stream, session, callback) {
        let chunks = [];
        let chunklen = 0;

        stream.on('readable', () => {
            let chunk;
            while ((chunk = stream.read()) !== null) {
                chunks.push(chunk);
                chunklen += chunk.length;
            }
        });

        stream.once('error', err => {
            log.error('LMTP', err);
            callback(new Error('Error reading from stream'));
        });

        stream.once('end', () => {
            let sender = tools.normalizeAddress((session.envelope.mailFrom && session.envelope.mailFrom.address) || '');
            let responses = [];
            let users = session.users;
            let stored = 0;

            let transactionId = new ObjectID();
            let prepared = false;

            let storeNext = () => {
                if (stored >= users.length) {
                    return callback(
                        null,
                        responses.map(r => r.response)
                    );
                }

                let rcptData = users[stored++];
                let recipient = rcptData.recipient;
                let userData = rcptData.user;

                let response = responses.filter(r => r.userData === userData);
                if (response.length) {
                    responses.push(response[0]);
                    return storeNext();
                }

                filterHandler.process(
                    {
                        mimeTree: prepared && prepared.mimeTree,
                        maildata: prepared && prepared.maildata,
                        user: userData,
                        sender,
                        recipient,
                        chunks,
                        chunklen,
                        meta: {
                            transactionId,
                            source: 'MX',
                            from: sender,
                            to: [recipient],
                            origin: session.remoteAddress,
                            originhost: session.clientHostname,
                            transhost: session.hostNameAppearsAs,
                            transtype: session.transmissionType,
                            time: new Date()
                        }
                    },
                    (err, response, preparedResponse) => {
                        if (err) {
                            // ???
                        }

                        if (response) {
                            responses.push(response);
                        }

                        if (!prepared && preparedResponse) {
                            prepared = preparedResponse;
                        }

                        setImmediate(storeNext);
                    }
                );
            };

            storeNext();
        });
    }
};

certs.loadTLSOptions(serverOptions, 'lmtp');

const server = new SMTPServer(serverOptions);

certs.registerReload(server, 'lmtp');

module.exports = done => {
    if (!config.lmtp.enabled) {
        return setImmediate(() => done(null, false));
    }

    const component = config.log.gelf.component || 'wildduck';
    const hostname = config.log.gelf.hostname || os.hostname();
    const gelf =
        config.log.gelf && config.log.gelf.enabled
            ? new Gelf(config.log.gelf.options)
            : {
                  // placeholder
                  emit: (key, message) => log.info('Gelf', JSON.stringify(message))
              };

    loggelf = message => {
        if (typeof message === 'string') {
            message = {
                short_message: message
            };
        }
        message = message || {};

        if (!message.short_message || message.short_message.indexOf(component.toUpperCase()) !== 0) {
            message.short_message = component.toUpperCase() + ' ' + (message.short_message || '');
        }

        message.facility = component; // facility is deprecated but set by the driver if not provided
        message.host = hostname;
        message.timestamp = Date.now() / 1000;
        message._component = component;
        Object.keys(message).forEach(key => {
            if (!message[key]) {
                delete message[key];
            }
        });
        gelf.emit('gelf.log', message);
    };

    messageHandler = new MessageHandler({
        database: db.database,
        users: db.users,
        redis: db.redis,
        gridfs: db.gridfs,
        attachments: config.attachments,
        loggelf: message => loggelf(message)
    });

    userHandler = new UserHandler({
        database: db.database,
        users: db.users,
        redis: db.redis,
        authlogExpireDays: config.log.authlogExpireDays,
        loggelf: message => loggelf(message)
    });

    filterHandler = new FilterHandler({
        db,
        sender: config.sender,
        messageHandler,
        loggelf: message => loggelf(message)
    });

    let started = false;

    server.on('error', err => {
        if (!started) {
            started = true;
            return done(err);
        }
        log.error('LMTP', err);
    });

    server.listen(config.lmtp.port, config.lmtp.host, () => {
        if (started) {
            return server.close();
        }
        started = true;
        done(null, server);
    });
};
