mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-03 00:07:05 +00:00
332 lines
9.7 KiB
Python
Executable File
332 lines
9.7 KiB
Python
Executable File
#!/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")
|