1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-03 00:07:05 +00:00
mailinabox/management/daemon_reports.py
2022-09-19 14:45:11 -04:00

276 lines
8.2 KiB
Python

# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
#####
##### 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.
#####
import os
import logging
import json
import datetime
import time
import subprocess
log = logging.getLogger(__name__)
from flask import request, Response, session, redirect, jsonify, send_from_directory
from functools import wraps
from reporting.capture.db.SqliteConnFactory import SqliteConnFactory
import reporting.uidata as uidata
from mailconfig import ( get_mail_users, validate_email )
def add_reports(app, env, authorized_personnel_only):
'''call this function to add reporting/sem endpoints
`app` is a Flask instance
`env` is the Mail-in-a-Box LDAP environment
`authorized_personnel_only` is a flask wrapper from daemon.py
ensuring only authenticated admins can access the endpoint
'''
CAPTURE_STORAGE_ROOT = os.environ.get(
'CAPTURE_STORAGE_ROOT',
os.path.join(env['STORAGE_ROOT'], 'reporting')
)
sqlite_file = os.path.join(CAPTURE_STORAGE_ROOT, 'capture.sqlite')
db_conn_factory = SqliteConnFactory(sqlite_file)
# UI support
ui_dir = os.path.join(os.path.dirname(app.template_folder), 'reporting/ui')
def send_ui_file(filename):
return send_from_directory(ui_dir, filename)
@app.route("/reports/ui/<path:filename>", methods=['GET'])
def get_reporting_ui_file(filename):
return send_ui_file(filename)
@app.route('/reports')
def reporting_redir():
return redirect('/reports/')
@app.route('/reports/', methods=['GET'])
def reporting_main():
return send_ui_file('index.html')
# Decorator to unwrap json payloads. It returns the json as a dict
# in named argument 'payload'
def json_payload(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
log.debug('payload:%s', request.data)
payload = json.loads(request.data)
return func(*args, payload=payload, **kwargs)
except json.decoder.JSONDecodeError as e:
log.warning('Bad request: data:%s ex:%s', request.data, e)
return ("Bad request", 400)
return wrapper
@app.route('/reports/uidata/messages-sent', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_data_chart_messages_sent(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.messages_sent(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/messages-received', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_data_chart_messages_received(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.messages_received(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/user-activity', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_data_user_activity(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.user_activity(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/imap-details', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_imap_details(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.imap_details(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/flagged-connections', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_data_flagged_connections(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.flagged_connections(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/remote-sender-activity', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_data_remote_sender_activity(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.remote_sender_activity(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/user-list', methods=['GET'])
@authorized_personnel_only
def get_data_user_list():
return jsonify(get_mail_users(env, as_map=False))
@app.route('/reports/uidata/select-list-suggestions', methods=['POST'])
@authorized_personnel_only
@json_payload
def suggest(payload):
conn = db_conn_factory.connect()
try:
return jsonify(uidata.select_list_suggestions(conn, payload))
except uidata.InvalidArgsError as e:
return ('invalid request', 400)
finally:
db_conn_factory.close(conn)
@app.route('/reports/capture/config', methods=['GET'])
@authorized_personnel_only
def get_capture_config():
try:
with open("/var/run/mailinabox/runtime_config.json") as fp:
return Response(fp.read(), mimetype="text/json")
except FileNotFoundError:
return jsonify({ 'status':'error', 'reason':'not running' })
@app.route('/reports/capture/config', methods=['POST'])
@authorized_personnel_only
@json_payload
def save_capture_config(payload):
try:
with open("/var/run/mailinabox/runtime_config.json") as fp:
loc = json.loads(fp.read()).get('from', { 'type':'unknown' })
except FileNotFoundError:
return ('service is not running', 403)
# loc: { type:'file', location:'<path>' }
if loc.get('type') != 'file':
return ('storage type is %s' % loc.get('type'), 403)
log.warning('overwriting config file %s', loc['location'])
# remove runtime-config extra fields that don't belong in
# the user config
if 'from' in payload:
del payload['from']
with open(loc['location'], "w") as fp:
fp.write(json.dumps(payload, indent=4))
r = subprocess.run(["systemctl", "reload", "miabldap-capture"])
if r.returncode != 0:
log.warning('systemctl reload failed for miabldap-capture: code=%s', r.returncode)
else:
# wait a sec for daemon to pick up new config
# TODO: monitor runtime config for mtime change
time.sleep(1)
# invalidate stats cache. if prune policy changed, the stats
# may be invalid
uidata.clear_cache()
return ("ok", 200)
@app.route('/reports/capture/service/status', methods=['GET'])
@authorized_personnel_only
def get_capture_status():
service = "miabldap-capture.service"
if not os.path.exists("/etc/systemd/system/" + service):
return jsonify([ 'not installed', 'not installed' ])
r1 = subprocess.run(["systemctl", "is-active", "--quiet", service ])
r2 = subprocess.run(["systemctl", "is-enabled", "--quiet", service ])
return jsonify([
'running' if r1.returncode == 0 else 'stopped',
'enabled' if r2.returncode == 0 else 'disabled'
])
@app.route('/reports/capture/db/stats', methods=['GET'])
@authorized_personnel_only
def get_db_stats():
conn = db_conn_factory.connect()
try:
return jsonify(uidata.capture_db_stats(conn))
finally:
db_conn_factory.close(conn)
@app.route('/reports/uidata/message-headers', methods=['POST'])
@authorized_personnel_only
@json_payload
def get_message_headers(payload):
try:
user_id = payload['user_id']
lmtp_id = payload['lmtp_id']
except KeyError:
return ('invalid request', 400)
if not validate_email(user_id, mode="user"):
return ('invalid email address', 400)
r = subprocess.run(
[
"/usr/bin/doveadm",
"fetch",
"-u",user_id,
"hdr",
"HEADER","received","LMTP id " + lmtp_id
],
encoding="utf8",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if r.returncode != 0:
log.error('retrieving message headers failed, code=%s, lmtp_id=%s, user_id=%s, stderr=%s', r.returncode, lmtp_id, user_id, r.stderr)
return Response(r.stderr, status=400, mimetype='text/plain')
else:
out = r.stdout.strip()
if out.startswith('hdr:\n'):
out = out[5:]
return Response(out, status=200, mimetype='text/plain')