+
+
+
+
@@ -61,7 +65,7 @@
- Remote host: {{ row.item.remote_host }}[{{ row.item.remote_ip }}]
+ Remote sender: {{ row.item.remote_host }}[{{ row.item.remote_ip }}]
Connection disposition: {{ disposition_formatter(row.item.disposition) }}
Sent to alias: {{ row.item.orig_to }}
Dkim reason: {{row.item.dkim_reason}}
@@ -69,7 +73,9 @@
Postgrey reason: {{row.item.postgrey_reason}}
Postgrey delay: {{received_mail.x_fields.postgrey_delay.formatter(row.item.postgrey_delay)}}
Spam score: {{received_mail.x_fields.spam_score.formatter(row.item.spam_score)}}
+ Message-ID: {{ row.item.message_id }}
Failure info: {{row.item.failure_info}}
+
diff --git a/management/reporting/ui/panel-user-activity.js b/management/reporting/ui/panel-user-activity.js
index 7542b523..53ef6e54 100644
--- a/management/reporting/ui/panel-user-activity.js
+++ b/management/reporting/ui/panel-user-activity.js
@@ -2,7 +2,13 @@
details on the activity of a user
*/
-Vue.component('panel-user-activity', function(resolve, reject) {
+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";
+
+
+export default Vue.component('panel-user-activity', function(resolve, reject) {
axios.get('reports/ui/panel-user-activity.html').then((response) => { resolve({
template: response.data,
@@ -12,7 +18,8 @@ Vue.component('panel-user-activity', function(resolve, reject) {
},
components: {
- 'wbr-text': Vue.component('wbr-text'),
+ 'wbr-text': wbr_text,
+ 'message-headers-view': message_headers_view
},
data: function() {
@@ -30,6 +37,7 @@ 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,
};
@@ -145,7 +153,9 @@ 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(
@@ -286,6 +296,15 @@ 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/ui/reports-page-header.js b/management/reporting/ui/reports-page-header.js
index 538b6fc6..2780387f 100644
--- a/management/reporting/ui/reports-page-header.js
+++ b/management/reporting/ui/reports-page-header.js
@@ -1,10 +1,12 @@
-Vue.component('reports-page-header', {
+import page_header from '../../ui-common/page-header.js';
+
+export default Vue.component('reports-page-header', {
props: {
loading_counter: { type:Number, required:true },
},
components: {
- 'page-header': Vue.component('page-header'),
+ 'page-header': page_header,
},
template:
diff --git a/management/reporting/ui/settings.js b/management/reporting/ui/settings.js
index 297e7d57..d543d473 100644
--- a/management/reporting/ui/settings.js
+++ b/management/reporting/ui/settings.js
@@ -1,6 +1,9 @@
+import { ValueError } from '../../ui-common/exceptions.js';
+
+
window.miabldap = window.miabldap || {};
-class CaptureConfig {
+export class CaptureConfig {
static get() {
return axios.get('/reports/capture/config').then(response => {
var cc = new CaptureConfig();
@@ -11,7 +14,7 @@ class CaptureConfig {
};
-class UserSettings {
+export default class UserSettings {
static load() {
if (window.miabldap.user_settings) {
return Promise.resolve(window.miabldap.user_settings);
diff --git a/management/reporting/ui/wbr-text.js b/management/reporting/ui/wbr-text.js
index e661681a..6727d035 100644
--- a/management/reporting/ui/wbr-text.js
+++ b/management/reporting/ui/wbr-text.js
@@ -11,7 +11,7 @@
* browser to wrap at any character of the text.
*/
-Vue.component('wbr-text', {
+export default Vue.component('wbr-text', {
props: {
text: { type:String, required: true },
break_chars: { type:String, default:'@_.,:+=' },
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)
diff --git a/management/ui-common/authentication.js b/management/ui-common/authentication.js
index 90dbeb7d..b7e0d940 100644
--- a/management/ui-common/authentication.js
+++ b/management/ui-common/authentication.js
@@ -1,4 +1,7 @@
-class Me {
+import { AuthenticationError } from './exceptions.js';
+
+
+export class Me {
/* construct with return value from GET /me */
constructor(me) {
Object.assign(this, me);
@@ -18,7 +21,7 @@ class Me {
* axios interceptors for authentication
*/
-function init_axios_interceptors() {
+export function init_authentication_interceptors() {
// requests: attach non-session based auth (admin panel)
axios.interceptors.request.use(request => {
@@ -38,7 +41,8 @@ function init_axios_interceptors() {
});
- // reponses: redirect on authorization failure
+ // reponses: handle authorization failures by throwing exceptions
+ // users should catch AuthenticationError exceptions
axios.interceptors.response.use(
response => {
if (response.data &&
diff --git a/management/ui-common/exceptions.js b/management/ui-common/exceptions.js
index 2ff8588d..c4396c73 100644
--- a/management/ui-common/exceptions.js
+++ b/management/ui-common/exceptions.js
@@ -1,13 +1,13 @@
-class ValueError extends Error {
+export class ValueError extends Error {
constructor(msg) {
super(msg);
}
};
-class AssertionError extends Error {
+export class AssertionError extends Error {
}
-class AuthenticationError extends Error {
+export class AuthenticationError extends Error {
constructor(caused_by_error, msg, response) {
super(msg);
this.caused_by = caused_by_error;
diff --git a/management/ui-common/page-header.js b/management/ui-common/page-header.js
index 2374cb0d..a1b9a19e 100644
--- a/management/ui-common/page-header.js
+++ b/management/ui-common/page-header.js
@@ -1,8 +1,8 @@
-Vue.component('spinner', {
+var spinner = Vue.component('spinner', {
template: ''
});
-Vue.component('page-header', function(resolve, reject) {
+var header = Vue.component('page-header', function(resolve, reject) {
axios.get('ui-common/page-header.html').then((response) => { resolve({
props: {
@@ -17,3 +17,5 @@ Vue.component('page-header', function(resolve, reject) {
});
});
+
+export { spinner, header as default };
diff --git a/management/ui-common/page-layout.js b/management/ui-common/page-layout.js
index 7af06a39..974edc97 100644
--- a/management/ui-common/page-layout.js
+++ b/management/ui-common/page-layout.js
@@ -1,4 +1,4 @@
-Vue.component('page-layout', function(resolve, reject) {
+export default Vue.component('page-layout', function(resolve, reject) {
axios.get('ui-common/page-layout.html').then((response) => { resolve({
template: response.data,