#!/usr/bin/env python3 from logs.TailFile import TailFile from mail.InboundMailLogHandler import InboundMailLogHandler 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", '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=='-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 ) inbound_mail_handler = InboundMailLogHandler( event_store, capture_enabled = options['config'].get('capture',True), drop_disposition = options['config'].get('drop_disposition') ) mail_tail.add_handler(inbound_mail_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() 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") # 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 records are in-progress', inbound_mail_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'] ) inbound_mail_handler.set_capture_enabled( newconfig.get('capture', True) ) inbound_mail_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()