#!/usr/bin/env python3
#####
##### This file is part of Mail-in-a-Box-LDAP which is released under the
##### terms of the GNU Affero General Public License as published by the
##### Free Software Foundation, either version 3 of the License, or (at
##### your option) any later version. See file LICENSE or go to
##### https://github.com/downtownallday/mailinabox-ldap for full license
##### details.
#####

from logs.TailFile import TailFile
from mail.PostfixLogHandler import PostfixLogHandler
from mail.DovecotLogHandler import DovecotLogHandler
from logs.ReadPositionStoreInFile import ReadPositionStoreInFile
from db.SqliteConnFactory import SqliteConnFactory
from db.SqliteEventStore import SqliteEventStore
from db.Pruner import Pruner
from util.env import load_env_vars_from_file

import time
import logging, logging.handlers
import json
import re
import os
import sys
import signal

log = logging.getLogger(__name__)

if os.path.exists('/etc/mailinabox.conf'):
    env = load_env_vars_from_file('/etc/mailinabox.conf')
else:
    env = { 'STORAGE_ROOT': os.environ.get('STORAGE_ROOT', os.getcwd()) }

CAPTURE_STORAGE_ROOT = os.path.join(env['STORAGE_ROOT'], 'reporting')


# default configuration, if not specified or
# CAPTURE_STORAGE_ROOT/config.json does not exist
config_default = {
    'default_config': True,
    'capture': True,
    'prune_policy': {
        'frequency_min': 2400,
        'older_than_days': 30
    },
    'drop_disposition': {
        'failed_login_attempt': True,
        'suspected_scanner': False,
        'reject': True
    }
}

config_default_file = os.path.join(CAPTURE_STORAGE_ROOT, 'config.json')


# options
    
options = {
    'daemon': True,
    'log_level': logging.WARNING,
    'log_file': "/var/log/mail.log",
    'stop_at_eof': False,
    'pos_file': "/var/lib/mailinabox/capture-pos.json",
    'sqlite_file': os.path.join(CAPTURE_STORAGE_ROOT, 'capture.sqlite'),
    'working_dir': "/var/run/mailinabox",
    'config': config_default,
    '_config_file': False,  # absolute path if "config" was from a file
    '_runtime_config_file': "runtime_config.json" # relative to working_dir
}


def read_config_file(file, throw=False):
    try:
        with open(file) as fp:
            newconfig = json.loads(fp.read())
        newconfig['from'] = { 'type':'file', 'location':file }
        return newconfig
    except FileNotFoundError as e:
        if not throw:
            return False
        raise e

def write_config(tofile, config):
    d = os.path.dirname(tofile)
    if d and not os.path.exists(d):
        os.mkdir(d, mode=0o770)
    with open(tofile, "w") as fp:
        fp.write(json.dumps(config))

def usage():
    print('usage: %s [options]' % sys.argv[0])
    sys.exit(1)

def process_cmdline(options):
    argi = 1
    marg = 0
    while argi < len(sys.argv):
        arg = sys.argv[argi]
        have_next = ( argi+1 < len(sys.argv) )
        
        if arg=='-d':
            options['daemon'] = False
                
        elif arg=='-loglevel' and have_next:
            argi += 1
            arg = sys.argv[argi].lower()
            if arg=='info':
                options['log_level'] = logging.INFO
            elif arg=='warning':
                options['log_level'] = logging.WARNING
            elif arg=='debug':
                options['log_level'] = logging.DEBUG
            elif arg=='error':
                options['log_level'] = logging.ERROR
            else:
                sys.stderr.write('unknown log level "%s"\n' % sys.argv[argi])
                
        elif arg=='-config' and have_next:
            argi += 1
            arg = sys.argv[argi]
            try:
                if arg.startswith('{'):
                    options['config'] = json.loads(arg)
                else:
                    newconfig = read_config_file(arg, throw=True)
                    options['config'] = newconfig
                    options['_config_file'] = arg
            except Exception as e:
                if options['daemon']: log.exception(e)
                raise e

        elif arg=='-logfile' and have_next:
            argi+=1
            options['log_file'] = sys.argv[argi]

        elif arg=='-stopateof':
            argi+=1
            options['stop_at_eof'] = True
            
        elif arg=='-posfile' and have_next:
            argi+=1
            options['pos_file'] = sys.argv[argi]
            
        elif arg=='-sqlitefile' and have_next:
            argi+=1
            options['sqlite_file'] = sys.argv[argi]            

        elif arg.startswith('-'):
            usage()

        else:
            if marg==0:
                options['log_file'] = arg
            elif marg==1:
                options['pos_file'] = arg
            elif marg==2:
                options['sqlite_file'] = arg
            else:
                usage()
            marg += 1
        argi += 1


def set_working_dir(working_dir):
    try:
        if not os.path.exists(working_dir):
            os.mkdir(working_dir, mode=0o770)
        os.chdir(working_dir)
    except Exception as e:
        log.exception(e)
        raise e

def close_stdio():
    sys.stdout.close()
    sys.stderr.close()
    sys.stdin.close()

    
# if config.json exists in the default location start with that
# instead of `config_default`. config can still be changed with the
# command line argument "-config"

newconfig = read_config_file(config_default_file, throw=False)
if newconfig:
    options['config'] = newconfig
    options['_config_file'] = config_default_file
    
# process command line
process_cmdline(options)


# init logging & set working directory
if options['daemon']:
    logging.basicConfig(
        level = options['log_level'],
        handlers= [
            logging.handlers.SysLogHandler(address='/dev/log')
        ],
        format = 'miabldap/capture %(message)s'
    )
    log.warning('MIAB-LDAP capture/SEM daemon starting: wd=%s; log_file=%s; pos_file=%s; db=%s',
                options['working_dir'],
                options['log_file'],
                options['pos_file'],
                options['sqlite_file']
    )
    close_stdio()
    set_working_dir(options['working_dir'])
    
else:
    logging.basicConfig(level=options['log_level'])
    log.info('starting: log_file=%s; pos_file=%s; db=%s',
             options['log_file'],
             options['pos_file'],
             options['sqlite_file']
    )


# save runtime config
write_config(options['_runtime_config_file'], options['config'])


# start modules
log.info('config: %s', options['config'])
try:
    db_conn_factory = SqliteConnFactory(
        options['sqlite_file']
    )
    event_store = SqliteEventStore(
        db_conn_factory
    )
    position_store = ReadPositionStoreInFile(
        options['pos_file']
    )
    mail_tail = TailFile(
        options['log_file'],
        position_store,
        options['stop_at_eof']
    )
    postfix_log_handler = PostfixLogHandler(
        event_store,
        capture_enabled = options['config'].get('capture',True),
        drop_disposition = options['config'].get('drop_disposition')
    )
    mail_tail.add_handler(postfix_log_handler)
    dovecot_log_handler = DovecotLogHandler(
        event_store,
        capture_enabled = options['config'].get('capture',True),
        drop_disposition = options['config'].get('drop_disposition')
    )
    mail_tail.add_handler(dovecot_log_handler)
    pruner = Pruner(
        db_conn_factory,
        policy=options['config']['prune_policy']
    )
    pruner.add_prunable(event_store)
    
except Exception as e:
    if options['daemon']: log.exception(e)
    raise e


# termination handler for graceful shutdowns
def terminate(sig, stack):
    if sig == signal.SIGTERM:
        log.warning("shutting down due to SIGTERM")
    log.debug("stopping mail_tail")
    mail_tail.stop()

# reload settings handler
def reload(sig, stack):
    # if the default config (`config_default`) is in use, check to see
    # if a default config.json (`config_default_file`) now exists, and
    # if so, use that
    if options['config'].get('default_config', False) and os.path.exists(config_default_file):
        options['config']['default_config'] = False
        options['_config_file'] = config_default_file

    log.info('%s mta records are in-progress',
             postfix_log_handler.get_inprogress_count())
    log.info('%s imap records are in-progress',
             dovecot_log_handler.get_inprogress_count())

    if options['_config_file']:
        log.info('reloading %s', options['_config_file'])
        try:
            newconfig = read_config_file(options['_config_file'], throw=True)
            pruner.set_policy(
                newconfig['prune_policy']
            )
            postfix_log_handler.set_capture_enabled(
                newconfig.get('capture', True)
            )
            postfix_log_handler.update_drop_disposition(
                newconfig.get('drop_disposition', {})
            )
            dovecot_log_handler.set_capture_enabled(
                newconfig.get('capture', True)
            )
            dovecot_log_handler.update_drop_disposition(
                newconfig.get('drop_disposition', {})
            )
            write_config(options['_runtime_config_file'], newconfig)
        except Exception as e:
            if options['daemon']:
                log.exception(e)
            else:
                raise e

signal.signal(signal.SIGTERM, terminate)
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGHUP, reload)


# monitor and capture
mail_tail.start()
mail_tail.join()

# gracefully close other threads
log.debug("stopping pruner")
pruner.stop()
log.debug("stopping position_store")
position_store.stop()
log.debug("stopping event_store")
event_store.stop()
try:
    os.remove(options['_runtime_config_file'])
except Exception:
    pass
log.info("stopped")