1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-06 00:37:06 +00:00

Merge branch 'reporting' into postgrey-whitelist

This commit is contained in:
downtownallday 2021-04-10 16:50:30 -04:00
commit 9c87884837
27 changed files with 256 additions and 110 deletions

View File

@ -14,7 +14,7 @@ from functools import wraps
from reporting.capture.db.SqliteConnFactory import SqliteConnFactory from reporting.capture.db.SqliteConnFactory import SqliteConnFactory
import reporting.uidata as uidata 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): 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"]) r = subprocess.run(["systemctl", "reload", "miabldap-capture"])
if r.returncode != 0: 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: else:
# wait a sec for daemon to pick up new config # wait a sec for daemon to pick up new config
# TODO: monitor runtime config for mtime change # 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)) return jsonify(uidata.capture_db_stats(conn))
finally: finally:
db_conn_factory.close(conn) 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')

View File

@ -1,9 +1,14 @@
import { BvTable, ConnectionDisposition, DateFormatter } from "./charting.js";
import { spinner } from "../../ui-common/page-header.js";
export default Vue.component('capture-db-stats', {
Vue.component('capture-db-stats', {
props: { props: {
}, },
components: {
spinner,
},
template:'<div>'+ template:'<div>'+
'<template v-if="stats">'+ '<template v-if="stats">'+
'<caption class="text-nowrap">Database date range</caption><div class="ml-2">First: {{stats.db_stats.connect_time.min_str}}</div><div class="ml-2">Last: {{stats.db_stats.connect_time.max_str}}</div>'+ '<caption class="text-nowrap">Database date range</caption><div class="ml-2">First: {{stats.db_stats.connect_time.min_str}}</div><div class="ml-2">Last: {{stats.db_stats.connect_time.max_str}}</div>'+

View File

@ -1,4 +1,6 @@
Vue.component('chart-multi-line-timeseries', { import { ChartPrefs, NumberFormatter, ChartVue } from "./charting.js";
export default Vue.component('chart-multi-line-timeseries', {
props: { props: {
chart_data: { type:Object, required:false }, /* TimeseriesData */ chart_data: { type:Object, required:false }, /* TimeseriesData */
width: { type:Number, default: ChartPrefs.default_width }, width: { type:Number, default: ChartPrefs.default_width },
@ -175,7 +177,7 @@ Vue.component('chart-multi-line-timeseries', {
const yvalue = this.yscale.invert(pointer[1]); // number const yvalue = this.yscale.invert(pointer[1]); // number
//const i = d3.bisectCenter(this.tsdata.dates, xvalue); // index //const i = d3.bisectCenter(this.tsdata.dates, xvalue); // index
var i = d3.bisect(this.tsdata.dates, xvalue); // index var i = d3.bisect(this.tsdata.dates, xvalue); // index
if (i > this.tsdata.dates.length) return; if (i<0 || i > this.tsdata.dates.length) return;
i = Math.min(this.tsdata.dates.length-1, i); i = Math.min(this.tsdata.dates.length-1, i);
// closest series // closest series
@ -190,7 +192,7 @@ Vue.component('chart-multi-line-timeseries', {
} }
} }
const s = this.tsdata.series[closest.sidx]; const s = this.tsdata.series[closest.sidx];
if (i>= s.values.length) { if (i<0 || i>= s.values.length) {
dot.attr("display", "none"); dot.attr("display", "none");
return; return;
} }

View File

@ -1,4 +1,7 @@
Vue.component('chart-pie', { import { ChartPrefs, NumberFormatter, ChartVue } from "./charting.js";
export default Vue.component('chart-pie', {
/* /*
* chart_data: [ * chart_data: [
* { name: 'name', value: value }, * { name: 'name', value: value },
@ -127,7 +130,7 @@ Vue.component('chart-pie', {
radius *= 0.7; radius *= 0.7;
else else
radius *= 0.8; radius *= 0.8;
arcLabel = d3.arc().innerRadius(radius).outerRadius(radius); var arcLabel = d3.arc().innerRadius(radius).outerRadius(radius);
svg.append("g") svg.append("g")
.attr("stroke", "white") .attr("stroke", "white")

View File

@ -2,7 +2,10 @@
stacked bar chart stacked bar chart
*/ */
Vue.component('chart-stacked-bar-timeseries', { import { ChartPrefs, NumberFormatter, ChartVue } from "./charting.js";
export default Vue.component('chart-stacked-bar-timeseries', {
props: { props: {
chart_data: { type:Object, required:false }, /* TimeseriesData */ chart_data: { type:Object, required:false }, /* TimeseriesData */
width: { type:Number, default: ChartPrefs.default_width }, width: { type:Number, default: ChartPrefs.default_width },

View File

@ -1,4 +1,4 @@
Vue.component('chart-table', { export default Vue.component('chart-table', {
props: { props: {
items: Array, items: Array,
fields: Array, fields: Array,
@ -38,7 +38,7 @@ Vue.component('chart-table', {
if (this.items.length == 0) { if (this.items.length == 0) {
return [{ return [{
key: 'no data', key: 'no data',
thClass: 'text-nowrap' thClass: 'text-nowrap align-top'
}]; }];
} }
return this.fields; return this.fields;

View File

@ -1,5 +1,5 @@
class ChartPrefs { export class ChartPrefs {
static get colors() { static get colors() {
// see: https://github.com/d3/d3-scale-chromatic // see: https://github.com/d3/d3-scale-chromatic
return d3.schemeSet2; return d3.schemeSet2;
@ -40,7 +40,7 @@ class ChartPrefs {
}; };
class DateFormatter { export class DateFormatter {
/* /*
* date and time * date and time
*/ */
@ -162,7 +162,7 @@ class DateFormatter {
}; };
class DateRange { export class DateRange {
/* /*
* ranges * ranges
*/ */
@ -241,7 +241,7 @@ class DateRange {
}; };
class NumberFormatter { export class NumberFormatter {
static format(v) { static format(v) {
return isNaN(v) || v===null ? "N/A" : v.toLocaleString(ChartPrefs.locales); return isNaN(v) || v===null ? "N/A" : v.toLocaleString(ChartPrefs.locales);
} }
@ -319,7 +319,7 @@ class NumberFormatter {
}); });
class BvTable { export class BvTable {
constructor(data, opt) { constructor(data, opt) {
opt = opt || {}; opt = opt || {};
Object.assign(this, data); Object.assign(this, data);
@ -446,7 +446,7 @@ class BvTable {
}; };
class BvTableField { export class BvTableField {
constructor(field, field_type) { constructor(field, field_type) {
// this: // this:
// key - required // key - required
@ -628,7 +628,7 @@ class BvTableField {
}; };
class MailBvTable extends BvTable { export class MailBvTable extends BvTable {
flag(key, fn) { flag(key, fn) {
var field = this.get_field(key, true); var field = this.get_field(key, true);
if (!field) return; if (!field) return;
@ -736,7 +736,7 @@ class MailBvTable extends BvTable {
} }
class ChartVue { export class ChartVue {
static svg_attrs(viewBox) { static svg_attrs(viewBox) {
var attrs = { var attrs = {
@ -818,7 +818,7 @@ class ChartVue {
* } * }
*/ */
class TimeseriesData { export class TimeseriesData {
constructor(data) { constructor(data) {
Object.assign(this, data); Object.assign(this, data);
this.convert_dates(); this.convert_dates();
@ -984,7 +984,7 @@ class TimeseriesData {
}; };
class ConnectionDisposition { export class ConnectionDisposition {
constructor(disposition) { constructor(disposition) {
const data = { const data = {
'failed_login_attempt': { 'failed_login_attempt': {

View File

@ -1,4 +1,7 @@
Vue.component('date-range-picker', { import { DateRange, DateFormatter } from "./charting.js";
export default Vue.component('date-range-picker', {
props: { props: {
start_range: [ String, Array ], // "ytd", "mtd", "wtd", or [start, end] where start and end are strings in format YYYY-MM-DD in localti start_range: [ String, Array ], // "ytd", "mtd", "wtd", or [start, end] where start and end are strings in format YYYY-MM-DD in localti
recall_id: String, // save / recall from localStorage recall_id: String, // save / recall from localStorage

View File

@ -22,34 +22,10 @@
<script src="https://d3js.org/d3.v6.min.js"></script> <script src="https://d3js.org/d3.v6.min.js"></script>
<script>axios.defaults.baseURL="/admin"</script> <script>axios.defaults.baseURL="/admin"</script>
<script src="reports/ui/index.js" type="module"></script>
<!-- our code -->
<script src="ui-common/exceptions.js"></script>
<script src="ui-common/authentication.js"></script>
<script src="ui-common/page-layout.js"></script>
<script src="ui-common/page-header.js"></script>
<script src="reports/ui/settings.js"></script>
<script src="reports/ui/capture-db-stats.js"></script>
<script src="reports/ui/charting.js"></script>
<script src="reports/ui/wbr-text.js"></script>
<script src="reports/ui/date-range-picker.js"></script>
<script src="reports/ui/chart-pie.js"></script>
<script src="reports/ui/chart-table.js"></script>
<script src="reports/ui/chart-stacked-bar-timeseries.js"></script>
<script src="reports/ui/chart-multi-line-timeseries.js"></script>
<script src="reports/ui/panel-messages-sent.js"></script>
<script src="reports/ui/panel-messages-received.js"></script>
<script src="reports/ui/panel-flagged-connections.js"></script>
<script src="reports/ui/panel-user-activity.js"></script>
<script src="reports/ui/panel-remote-sender-activity.js"></script>
<script src="reports/ui/reports-page-header.js"></script>
<script src="reports/ui/page-settings.js"></script>
<script src="reports/ui/page-reports-main.js"></script>
<script src="reports/ui/index.js"></script>
</head> </head>
<body onload="init_app()"> <body>
<router-view id="app"></router-view> <router-view id="app"></router-view>

View File

@ -2,14 +2,19 @@
* reports index page * reports index page
*/ */
import page_settings from "./page-settings.js";
import page_reports_main from "./page-reports-main.js";
import { Me, init_authentication_interceptors } from "../../ui-common/authentication.js";
import { AuthenticationError } from "../../ui-common/exceptions.js";
import UserSettings from "./settings.js";
const app = { const app = {
router: new VueRouter({ router: new VueRouter({
routes: [ routes: [
{ path: '/', component: Vue.component('page-reports-main') }, { path: '/', component: page_reports_main },
{ path: '/settings', component: Vue.component('page-settings') }, { path: '/settings', component: page_settings },
{ path: '/:panel', component: Vue.component('page-reports-main') }, { path: '/:panel', component: page_reports_main },
], ],
scrollBehavior: function(to, from, savedPosition) { scrollBehavior: function(to, from, savedPosition) {
if (savedPosition) { if (savedPosition) {
@ -19,8 +24,8 @@ const app = {
}), }),
components: { components: {
'page-settings': Vue.component('page-settings'), 'page-settings': page_settings,
'page-reports-main': Vue.component('page-reports-main'), 'page-reports-main': page_reports_main,
}, },
data: { data: {
@ -64,12 +69,12 @@ const app = {
function init_app() {
init_axios_interceptors(); init_authentication_interceptors();
UserSettings.load().then(settings => { UserSettings.load().then(settings => {
new Vue(app).$mount('#app'); new Vue(app).$mount('#app');
}).catch(error => { }).catch(error => {
alert('' + error); alert('' + error);
}); });
}

View File

@ -1,16 +1,31 @@
Vue.component('page-reports-main', function(resolve, reject) { import page_layout from '../../ui-common/page-layout.js';
import reports_page_header from './reports-page-header.js';
import date_range_picker from './date-range-picker.js';
import capture_db_stats from './capture-db-stats.js';
import panel_messages_sent from './panel-messages-sent.js';
import panel_messages_received from './panel-messages-received.js';
import panel_flagged_connections from './panel-flagged-connections.js';
import panel_user_activity from './panel-user-activity.js';
import panel_remote_sender_activity from './panel-remote-sender-activity.js';
import { TimeseriesData } from './charting.js';
export default Vue.component('page-reports-main', function(resolve, reject) {
axios.get('reports/ui/page-reports-main.html').then((response) => { resolve({ axios.get('reports/ui/page-reports-main.html').then((response) => { resolve({
template: response.data, template: response.data,
components: { components: {
'page-layout': Vue.component('page-layout'), 'page-layout': page_layout,
'reports-page-header': Vue.component('reports-page-header'), 'reports-page-header': reports_page_header,
'date-range-picker': Vue.component('date-range-picker'), 'date-range-picker': date_range_picker,
'panel-messages-sent': Vue.component('panel-messages-sent'), 'capture-db-stats': capture_db_stats,
'panel-messages-received': Vue.component('panel-messages-received'), 'panel-messages-sent': panel_messages_sent,
'panel-flagged-connections': Vue.component('panel-flagged-connections'), 'panel-messages-received': panel_messages_received,
'panel-user-activity': Vue.component('panel-user-activity'), 'panel-flagged-connections': panel_flagged_connections,
'panel-user-activity': panel_user_activity,
'panel-remote-sender-activity': panel_remote_sender_activity,
}, },
data: function() { data: function() {

View File

@ -1,11 +1,18 @@
Vue.component('page-settings', function(resolve, reject) {
import page_layout from '../../ui-common/page-layout.js';
import reports_page_header from './reports-page-header.js';
import UserSettings from "./settings.js";
import { CaptureConfig } from "./settings.js";
export default Vue.component('page-settings', function(resolve, reject) {
axios.get('reports/ui/page-settings.html').then((response) => { resolve({ axios.get('reports/ui/page-settings.html').then((response) => { resolve({
template: response.data, template: response.data,
components: { components: {
'page-layout': Vue.component('page-layout'), 'page-layout': page_layout,
'reports-page-header': Vue.component('reports-page-header'), 'reports-page-header': reports_page_header,
}, },
data: function() { data: function() {

View File

@ -1,4 +1,13 @@
Vue.component('panel-flagged-connections', function(resolve, reject) {
import chart_multi_line_timeseries from "./chart-multi-line-timeseries.js";
import chart_stacked_bar_timeseries from "./chart-stacked-bar-timeseries.js";
import chart_pie from "./chart-pie.js";
import chart_table from "./chart-table.js";
import { ChartPrefs, TimeseriesData, BvTable, ConnectionDisposition } from "./charting.js";
export default Vue.component('panel-flagged-connections', function(resolve, reject) {
axios.get('reports/ui/panel-flagged-connections.html').then((response) => { resolve({ axios.get('reports/ui/panel-flagged-connections.html').then((response) => { resolve({
template: response.data, template: response.data,
@ -14,10 +23,10 @@ Vue.component('panel-flagged-connections', function(resolve, reject) {
}, },
components: { components: {
'chart-multi-line-timeseries': Vue.component('chart-multi-line-timeseries'), 'chart-multi-line-timeseries': chart_multi_line_timeseries,
'chart-stacked-bar-timeseries': Vue.component('chart-stacked-bar-timeseries'), 'chart-stacked-bar-timeseries': chart_stacked_bar_timeseries,
'chart-pie': Vue.component('chart-pie'), 'chart-pie': chart_pie,
'chart-table': Vue.component('chart-table'), 'chart-table': chart_table,
}, },
computed: { computed: {

View File

@ -3,7 +3,13 @@
*/ */
Vue.component('panel-messages-received', function(resolve, reject) { import chart_multi_line_timeseries from "./chart-multi-line-timeseries.js";
import chart_table from "./chart-table.js";
import { ChartPrefs, TimeseriesData, BvTable } from "./charting.js";
export default Vue.component('panel-messages-received', function(resolve, reject) {
axios.get('reports/ui/panel-messages-received.html').then((response) => { resolve({ axios.get('reports/ui/panel-messages-received.html').then((response) => { resolve({
template: response.data, template: response.data,
@ -19,10 +25,8 @@ Vue.component('panel-messages-received', function(resolve, reject) {
}, },
components: { components: {
'chart-multi-line-timeseries': Vue.component('chart-multi-line-timeseries'), 'chart-multi-line-timeseries': chart_multi_line_timeseries,
// 'chart-stacked-bar-timeseries': Vue.component('chart-stacked-bar-timeseries'), 'chart-table': chart_table,
// 'chart-pie': Vue.component('chart-pie'),
'chart-table': Vue.component('chart-table'),
}, },
data: function() { data: function() {

View File

@ -9,7 +9,15 @@
'loading' event=number 'loading' event=number
*/ */
Vue.component('panel-messages-sent', function(resolve, reject) { import chart_multi_line_timeseries from "./chart-multi-line-timeseries.js";
import chart_stacked_bar_timeseries from "./chart-stacked-bar-timeseries.js";
import chart_pie from "./chart-pie.js";
import chart_table from "./chart-table.js";
import { ChartPrefs, TimeseriesData, BvTable } from "./charting.js";
export default Vue.component('panel-messages-sent', function(resolve, reject) {
axios.get('reports/ui/panel-messages-sent.html').then((response) => { resolve({ axios.get('reports/ui/panel-messages-sent.html').then((response) => { resolve({
template: response.data, template: response.data,
@ -27,10 +35,10 @@ Vue.component('panel-messages-sent', function(resolve, reject) {
}, },
components: { components: {
'chart-multi-line-timeseries': Vue.component('chart-multi-line-timeseries'), 'chart-multi-line-timeseries': chart_multi_line_timeseries,
'chart-stacked-bar-timeseries': Vue.component('chart-stacked-bar-timeseries'), 'chart-stacked-bar-timeseries': chart_stacked_bar_timeseries,
'chart-pie': Vue.component('chart-pie'), 'chart-pie': chart_pie,
'chart-table': Vue.component('chart-table'), 'chart-table': chart_table,
}, },
data: function() { data: function() {

View File

@ -2,7 +2,11 @@
details on the activity of a remote sender (envelope from) details on the activity of a remote sender (envelope from)
*/ */
Vue.component('panel-remote-sender-activity', function(resolve, reject) { import UserSettings from "./settings.js";
import { MailBvTable, ConnectionDisposition } from "./charting.js";
export default Vue.component('panel-remote-sender-activity', function(resolve, reject) {
axios.get('reports/ui/panel-remote-sender-activity.html').then((response) => { resolve({ axios.get('reports/ui/panel-remote-sender-activity.html').then((response) => { resolve({
template: response.data, template: response.data,

View File

@ -1,5 +1,9 @@
<div> <div>
<b-modal ref="message_headers_modal" hide-header no-fade ok-only no-close-on-backdrop size="lg" scrollable>
<message-headers-view :user_id="data_user_id" :lmtp_id="lmtp_id"></message-headers-view>
</b-modal>
<datalist id="panel-ua-users"> <datalist id="panel-ua-users">
<option v-for="user in all_users">{{ user }}</option> <option v-for="user in all_users">{{ user }}</option>
</datalist> </datalist>
@ -61,7 +65,7 @@
</template> </template>
<template #row-details="row"> <template #row-details="row">
<b-card> <b-card>
<div><strong>Remote host</strong>: {{ row.item.remote_host }}[{{ row.item.remote_ip }}]</div> <div><strong>Remote sender</strong>: {{ row.item.remote_host }}[{{ row.item.remote_ip }}]</div>
<div><strong>Connection disposition</strong>: {{ disposition_formatter(row.item.disposition) }}</div> <div><strong>Connection disposition</strong>: {{ disposition_formatter(row.item.disposition) }}</div>
<div v-if="row.item.orig_to"><strong>Sent to alias</strong>: {{ row.item.orig_to }}</div> <div v-if="row.item.orig_to"><strong>Sent to alias</strong>: {{ row.item.orig_to }}</div>
<div v-if="row.item.dkim_reason"><strong>Dkim reason</strong>: {{row.item.dkim_reason}}</div> <div v-if="row.item.dkim_reason"><strong>Dkim reason</strong>: {{row.item.dkim_reason}}</div>
@ -69,7 +73,9 @@
<div v-if="row.item.postgrey_reason"><strong>Postgrey reason</strong>: {{row.item.postgrey_reason}}</div> <div v-if="row.item.postgrey_reason"><strong>Postgrey reason</strong>: {{row.item.postgrey_reason}}</div>
<div v-if="row.item.postgrey_delay"><strong>Postgrey delay</strong>: {{received_mail.x_fields.postgrey_delay.formatter(row.item.postgrey_delay)}}</div> <div v-if="row.item.postgrey_delay"><strong>Postgrey delay</strong>: {{received_mail.x_fields.postgrey_delay.formatter(row.item.postgrey_delay)}}</div>
<div v-if="row.item.spam_result"><strong>Spam score</strong>: {{received_mail.x_fields.spam_score.formatter(row.item.spam_score)}}</div> <div v-if="row.item.spam_result"><strong>Spam score</strong>: {{received_mail.x_fields.spam_score.formatter(row.item.spam_score)}}</div>
<div v-if="row.item.message_id"><strong>Message-ID</strong>: {{ row.item.message_id }}</div>
<div v-if="row.item.failure_info"><strong>Failure info</strong>: {{row.item.failure_info}}</div> <div v-if="row.item.failure_info"><strong>Failure info</strong>: {{row.item.failure_info}}</div>
<div v-if="row.item.lmtp_id"><a href="#" @click.prevent.stop="show_message_headers(row.item.lmtp_id)">Message headers</a></div>
</b-card> </b-card>
</template> </template>
</b-table> </b-table>

View File

@ -2,7 +2,13 @@
details on the activity of a user 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({ axios.get('reports/ui/panel-user-activity.html').then((response) => { resolve({
template: response.data, template: response.data,
@ -12,7 +18,8 @@ Vue.component('panel-user-activity', function(resolve, reject) {
}, },
components: { components: {
'wbr-text': Vue.component('wbr-text'), 'wbr-text': wbr_text,
'message-headers-view': message_headers_view
}, },
data: function() { data: function() {
@ -30,6 +37,7 @@ Vue.component('panel-user-activity', function(resolve, reject) {
sent_mail: null, sent_mail: null,
received_mail: null, received_mail: null,
imap_details: null, imap_details: null,
lmtp_id: null, /* for message headers modal */
all_users: [], all_users: [],
disposition_formatter: ConnectionDisposition.formatter, disposition_formatter: ConnectionDisposition.formatter,
}; };
@ -145,7 +153,9 @@ Vue.component('panel-user-activity', function(resolve, reject) {
'postgrey_reason', 'postgrey_reason',
'postgrey_delay', 'postgrey_delay',
'spam_score', 'spam_score',
'orig_to' 'orig_to',
'message_id',
'lmtp_id',
]); ]);
// combine fields 'envelope_from' and 'sasl_username' // combine fields 'envelope_from' and 'sasl_username'
var f = this.received_mail.combine_fields( 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) { row_clicked: function(item, index, event) {
item._showDetails = ! item._showDetails; 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();
}
} }

View File

@ -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: { props: {
loading_counter: { type:Number, required:true }, loading_counter: { type:Number, required:true },
}, },
components: { components: {
'page-header': Vue.component('page-header'), 'page-header': page_header,
}, },
template: template:

View File

@ -1,6 +1,9 @@
import { ValueError } from '../../ui-common/exceptions.js';
window.miabldap = window.miabldap || {}; window.miabldap = window.miabldap || {};
class CaptureConfig { export class CaptureConfig {
static get() { static get() {
return axios.get('/reports/capture/config').then(response => { return axios.get('/reports/capture/config').then(response => {
var cc = new CaptureConfig(); var cc = new CaptureConfig();
@ -11,7 +14,7 @@ class CaptureConfig {
}; };
class UserSettings { export default class UserSettings {
static load() { static load() {
if (window.miabldap.user_settings) { if (window.miabldap.user_settings) {
return Promise.resolve(window.miabldap.user_settings); return Promise.resolve(window.miabldap.user_settings);

View File

@ -11,7 +11,7 @@
* browser to wrap at any character of the text. * browser to wrap at any character of the text.
*/ */
Vue.component('wbr-text', { export default Vue.component('wbr-text', {
props: { props: {
text: { type:String, required: true }, text: { type:String, required: true },
break_chars: { type:String, default:'@_.,:+=' }, break_chars: { type:String, default:'@_.,:+=' },

View File

@ -7,9 +7,9 @@ connect_time, mta_connection.service AS service, sasl_username, disposition,
remote_host, remote_ip, remote_host, remote_ip,
-- mta_accept -- mta_accept
envelope_from, spf_result, dkim_result, dkim_reason, dmarc_result, dmarc_reason, envelope_from, spf_result, dkim_result, dkim_reason, dmarc_result, dmarc_reason,
failure_info, message_id, failure_info,
-- mta_delivery -- 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 FROM mta_accept
JOIN mta_connection ON mta_accept.mta_conn_id = mta_connection.mta_conn_id 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 JOIN mta_delivery ON mta_accept.mta_accept_id = mta_delivery.mta_accept_id

View File

@ -124,6 +124,7 @@ def user_activity(conn, args):
'dkim_reason', 'dkim_reason',
'dmarc_result', 'dmarc_result',
'dmarc_reason', 'dmarc_reason',
'message_id',
'failure_info', 'failure_info',
# mta_delivery # mta_delivery
@ -134,6 +135,7 @@ def user_activity(conn, args):
'spam_score', 'spam_score',
'spam_result', 'spam_result',
'message_size', 'message_size',
'lmtp_id',
], ],
'field_types': [ 'field_types': [
{ 'type':'datetime', 'format': '%Y-%m-%d %H:%M:%S' },# connect_time { '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', # dkim_result
'text/plain', # dmarc_result 'text/plain', # dmarc_result
'text/plain', # dmarc_reason 'text/plain', # dmarc_reason
'text/plain', # message_id
'text/plain', # failure_info 'text/plain', # failure_info
'text/email', # orig_to 'text/email', # orig_to
'text/plain', # postgrey_result 'text/plain', # postgrey_result
@ -156,6 +159,7 @@ def user_activity(conn, args):
{ 'type':'decimal', 'places':2 }, # spam_score { 'type':'decimal', 'places':2 }, # spam_score
'text/plain', # spam_result 'text/plain', # spam_result
'number/size', # message_size 'number/size', # message_size
'text/plain', # lmtp_id
], ],
'items': [] 'items': []
} }
@ -167,7 +171,31 @@ def user_activity(conn, args):
}): }):
v = [] v = []
for key in received_mail['fields']: 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 <user@domain.tld> 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) received_mail['items'].append(v)

View File

@ -1,4 +1,7 @@
class Me { import { AuthenticationError } from './exceptions.js';
export class Me {
/* construct with return value from GET /me */ /* construct with return value from GET /me */
constructor(me) { constructor(me) {
Object.assign(this, me); Object.assign(this, me);
@ -18,7 +21,7 @@ class Me {
* axios interceptors for authentication * axios interceptors for authentication
*/ */
function init_axios_interceptors() { export function init_authentication_interceptors() {
// requests: attach non-session based auth (admin panel) // requests: attach non-session based auth (admin panel)
axios.interceptors.request.use(request => { 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( axios.interceptors.response.use(
response => { response => {
if (response.data && if (response.data &&

View File

@ -1,13 +1,13 @@
class ValueError extends Error { export class ValueError extends Error {
constructor(msg) { constructor(msg) {
super(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) { constructor(caused_by_error, msg, response) {
super(msg); super(msg);
this.caused_by = caused_by_error; this.caused_by = caused_by_error;

View File

@ -1,8 +1,8 @@
Vue.component('spinner', { var spinner = Vue.component('spinner', {
template: '<span class="spinner-border spinner-border-sm"></span>' template: '<span class="spinner-border spinner-border-sm"></span>'
}); });
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({ axios.get('ui-common/page-header.html').then((response) => { resolve({
props: { props: {
@ -17,3 +17,5 @@ Vue.component('page-header', function(resolve, reject) {
}); });
}); });
export { spinner, header as default };

View File

@ -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({ axios.get('ui-common/page-layout.html').then((response) => { resolve({
template: response.data, template: response.data,