diff --git a/management/daemon_reports.py b/management/daemon_reports.py index 26927900..fcebffcb 100644 --- a/management/daemon_reports.py +++ b/management/daemon_reports.py @@ -14,7 +14,7 @@ from functools import wraps from reporting.capture.db.SqliteConnFactory import SqliteConnFactory import reporting.uidata as uidata -from mailconfig import get_mail_users +from mailconfig import ( get_mail_users, validate_email ) def add_reports(app, env, authorized_personnel_only): @@ -178,7 +178,7 @@ def add_reports(app, env, authorized_personnel_only): r = subprocess.run(["systemctl", "reload", "miabldap-capture"]) if r.returncode != 0: - log.warning('systemctl reload faild for miabldap-capture: code=%s', r.returncode) + 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 @@ -214,3 +214,41 @@ def add_reports(app, env, authorized_personnel_only): 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') + + diff --git a/management/reporting/ui/panel-user-activity.html b/management/reporting/ui/panel-user-activity.html index b5acbb51..637a296e 100644 --- a/management/reporting/ui/panel-user-activity.html +++ b/management/reporting/ui/panel-user-activity.html @@ -1,5 +1,9 @@
+ + + + @@ -61,7 +65,7 @@ diff --git a/management/reporting/ui/panel-user-activity.js b/management/reporting/ui/panel-user-activity.js index 386f0311..53ef6e54 100644 --- a/management/reporting/ui/panel-user-activity.js +++ b/management/reporting/ui/panel-user-activity.js @@ -3,6 +3,7 @@ */ import wbr_text from "./wbr-text.js"; +import message_headers_view from "./message_headers_view.js"; import UserSettings from "./settings.js"; import { MailBvTable, ConnectionDisposition } from "./charting.js"; @@ -18,6 +19,7 @@ export default Vue.component('panel-user-activity', function(resolve, reject) { components: { 'wbr-text': wbr_text, + 'message-headers-view': message_headers_view }, data: function() { @@ -35,6 +37,7 @@ export default Vue.component('panel-user-activity', function(resolve, reject) { sent_mail: null, received_mail: null, imap_details: null, + lmtp_id: null, /* for message headers modal */ all_users: [], disposition_formatter: ConnectionDisposition.formatter, }; @@ -150,7 +153,9 @@ export default Vue.component('panel-user-activity', function(resolve, reject) { 'postgrey_reason', 'postgrey_delay', 'spam_score', - 'orig_to' + 'orig_to', + 'message_id', + 'lmtp_id', ]); // combine fields 'envelope_from' and 'sasl_username' var f = this.received_mail.combine_fields( @@ -291,6 +296,15 @@ export default Vue.component('panel-user-activity', function(resolve, reject) { row_clicked: function(item, index, event) { item._showDetails = ! item._showDetails; }, + + show_message_headers: function(lmtp_id) { + // set the lmtp_id that component message-headers-view + // searches for + this.lmtp_id = lmtp_id; + + // show the modal dialog + this.$refs.message_headers_modal.show(); + } } diff --git a/management/reporting/uidata/user_activity.2.sql b/management/reporting/uidata/user_activity.2.sql index 45614707..3f1f3004 100644 --- a/management/reporting/uidata/user_activity.2.sql +++ b/management/reporting/uidata/user_activity.2.sql @@ -7,9 +7,9 @@ connect_time, mta_connection.service AS service, sasl_username, disposition, remote_host, remote_ip, -- mta_accept envelope_from, spf_result, dkim_result, dkim_reason, dmarc_result, dmarc_reason, -failure_info, +message_id, failure_info, -- mta_delivery -postgrey_result, postgrey_reason, postgrey_delay, spam_score, spam_result, message_size, orig_to +postgrey_result, postgrey_reason, postgrey_delay, spam_score, spam_result, message_size, orig_to, delivery_info FROM mta_accept JOIN mta_connection ON mta_accept.mta_conn_id = mta_connection.mta_conn_id JOIN mta_delivery ON mta_accept.mta_accept_id = mta_delivery.mta_accept_id diff --git a/management/reporting/uidata/user_activity.py b/management/reporting/uidata/user_activity.py index ed135ba1..31221466 100644 --- a/management/reporting/uidata/user_activity.py +++ b/management/reporting/uidata/user_activity.py @@ -124,6 +124,7 @@ def user_activity(conn, args): 'dkim_reason', 'dmarc_result', 'dmarc_reason', + 'message_id', 'failure_info', # mta_delivery @@ -134,6 +135,7 @@ def user_activity(conn, args): 'spam_score', 'spam_result', 'message_size', + 'lmtp_id', ], 'field_types': [ { 'type':'datetime', 'format': '%Y-%m-%d %H:%M:%S' },# connect_time @@ -148,6 +150,7 @@ def user_activity(conn, args): 'text/plain', # dkim_result 'text/plain', # dmarc_result 'text/plain', # dmarc_reason + 'text/plain', # message_id 'text/plain', # failure_info 'text/email', # orig_to 'text/plain', # postgrey_result @@ -156,6 +159,7 @@ def user_activity(conn, args): { 'type':'decimal', 'places':2 }, # spam_score 'text/plain', # spam_result 'number/size', # message_size + 'text/plain', # lmtp_id ], 'items': [] } @@ -167,7 +171,31 @@ def user_activity(conn, args): }): v = [] for key in received_mail['fields']: - v.append(row[key]) + if key == 'lmtp_id': + # Extract the LMTP ID from delivery info, which looks + # like: + # + # "250 2.0.0 oPHmBDvTaWA7UwAAlWWVsw + # Saved" + # + # When we know the LMTP ID, we can get the message + # headers using doveadm, like this: + # + # "/usr/bin/doveadm fetch -u "user@domain.tld" hdr + # HEADER received "LMTP id oPHmBDvTaWA7UwAAlWWVsw" + # + delivery_info = row['delivery_info'] + valid = False + if delivery_info: + parts = delivery_info.split(' ') + if parts[0]=='250' and parts[1]=='2.0.0': + v.append(parts[-2]) + valid = True + if not valid: + v.append(None) + + else: + v.append(row[key]) received_mail['items'].append(v)