mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-05 00:27:25 +00:00
Merge branch 'reporting' into postgrey-whitelist
This commit is contained in:
commit
9c87884837
@ -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')
|
||||
|
||||
|
||||
|
@ -1,9 +1,14 @@
|
||||
import { BvTable, ConnectionDisposition, DateFormatter } from "./charting.js";
|
||||
import { spinner } from "../../ui-common/page-header.js";
|
||||
|
||||
|
||||
Vue.component('capture-db-stats', {
|
||||
export default Vue.component('capture-db-stats', {
|
||||
props: {
|
||||
},
|
||||
|
||||
components: {
|
||||
spinner,
|
||||
},
|
||||
|
||||
template:'<div>'+
|
||||
'<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>'+
|
||||
|
@ -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: {
|
||||
chart_data: { type:Object, required:false }, /* TimeseriesData */
|
||||
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 i = d3.bisectCenter(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);
|
||||
|
||||
// closest series
|
||||
@ -190,7 +192,7 @@ Vue.component('chart-multi-line-timeseries', {
|
||||
}
|
||||
}
|
||||
const s = this.tsdata.series[closest.sidx];
|
||||
if (i>= s.values.length) {
|
||||
if (i<0 || i>= s.values.length) {
|
||||
dot.attr("display", "none");
|
||||
return;
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
Vue.component('chart-pie', {
|
||||
import { ChartPrefs, NumberFormatter, ChartVue } from "./charting.js";
|
||||
|
||||
|
||||
export default Vue.component('chart-pie', {
|
||||
/*
|
||||
* chart_data: [
|
||||
* { name: 'name', value: value },
|
||||
@ -127,7 +130,7 @@ Vue.component('chart-pie', {
|
||||
radius *= 0.7;
|
||||
else
|
||||
radius *= 0.8;
|
||||
arcLabel = d3.arc().innerRadius(radius).outerRadius(radius);
|
||||
var arcLabel = d3.arc().innerRadius(radius).outerRadius(radius);
|
||||
|
||||
svg.append("g")
|
||||
.attr("stroke", "white")
|
||||
|
@ -2,7 +2,10 @@
|
||||
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: {
|
||||
chart_data: { type:Object, required:false }, /* TimeseriesData */
|
||||
width: { type:Number, default: ChartPrefs.default_width },
|
||||
|
@ -1,4 +1,4 @@
|
||||
Vue.component('chart-table', {
|
||||
export default Vue.component('chart-table', {
|
||||
props: {
|
||||
items: Array,
|
||||
fields: Array,
|
||||
@ -38,7 +38,7 @@ Vue.component('chart-table', {
|
||||
if (this.items.length == 0) {
|
||||
return [{
|
||||
key: 'no data',
|
||||
thClass: 'text-nowrap'
|
||||
thClass: 'text-nowrap align-top'
|
||||
}];
|
||||
}
|
||||
return this.fields;
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
class ChartPrefs {
|
||||
export class ChartPrefs {
|
||||
static get colors() {
|
||||
// see: https://github.com/d3/d3-scale-chromatic
|
||||
return d3.schemeSet2;
|
||||
@ -40,7 +40,7 @@ class ChartPrefs {
|
||||
};
|
||||
|
||||
|
||||
class DateFormatter {
|
||||
export class DateFormatter {
|
||||
/*
|
||||
* date and time
|
||||
*/
|
||||
@ -162,7 +162,7 @@ class DateFormatter {
|
||||
};
|
||||
|
||||
|
||||
class DateRange {
|
||||
export class DateRange {
|
||||
/*
|
||||
* ranges
|
||||
*/
|
||||
@ -241,7 +241,7 @@ class DateRange {
|
||||
};
|
||||
|
||||
|
||||
class NumberFormatter {
|
||||
export class NumberFormatter {
|
||||
static format(v) {
|
||||
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) {
|
||||
opt = opt || {};
|
||||
Object.assign(this, data);
|
||||
@ -446,7 +446,7 @@ class BvTable {
|
||||
};
|
||||
|
||||
|
||||
class BvTableField {
|
||||
export class BvTableField {
|
||||
constructor(field, field_type) {
|
||||
// this:
|
||||
// key - required
|
||||
@ -628,7 +628,7 @@ class BvTableField {
|
||||
};
|
||||
|
||||
|
||||
class MailBvTable extends BvTable {
|
||||
export class MailBvTable extends BvTable {
|
||||
flag(key, fn) {
|
||||
var field = this.get_field(key, true);
|
||||
if (!field) return;
|
||||
@ -736,7 +736,7 @@ class MailBvTable extends BvTable {
|
||||
}
|
||||
|
||||
|
||||
class ChartVue {
|
||||
export class ChartVue {
|
||||
|
||||
static svg_attrs(viewBox) {
|
||||
var attrs = {
|
||||
@ -818,7 +818,7 @@ class ChartVue {
|
||||
* }
|
||||
*/
|
||||
|
||||
class TimeseriesData {
|
||||
export class TimeseriesData {
|
||||
constructor(data) {
|
||||
Object.assign(this, data);
|
||||
this.convert_dates();
|
||||
@ -984,7 +984,7 @@ class TimeseriesData {
|
||||
};
|
||||
|
||||
|
||||
class ConnectionDisposition {
|
||||
export class ConnectionDisposition {
|
||||
constructor(disposition) {
|
||||
const data = {
|
||||
'failed_login_attempt': {
|
||||
|
@ -1,4 +1,7 @@
|
||||
Vue.component('date-range-picker', {
|
||||
import { DateRange, DateFormatter } from "./charting.js";
|
||||
|
||||
|
||||
export default Vue.component('date-range-picker', {
|
||||
props: {
|
||||
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
|
||||
|
@ -22,34 +22,10 @@
|
||||
<script src="https://d3js.org/d3.v6.min.js"></script>
|
||||
|
||||
<script>axios.defaults.baseURL="/admin"</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>
|
||||
<script src="reports/ui/index.js" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body onload="init_app()">
|
||||
<body>
|
||||
|
||||
<router-view id="app"></router-view>
|
||||
|
||||
|
@ -2,14 +2,19 @@
|
||||
* 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 = {
|
||||
router: new VueRouter({
|
||||
routes: [
|
||||
{ path: '/', component: Vue.component('page-reports-main') },
|
||||
{ path: '/settings', component: Vue.component('page-settings') },
|
||||
{ path: '/:panel', component: Vue.component('page-reports-main') },
|
||||
{ path: '/', component: page_reports_main },
|
||||
{ path: '/settings', component: page_settings },
|
||||
{ path: '/:panel', component: page_reports_main },
|
||||
],
|
||||
scrollBehavior: function(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
@ -19,8 +24,8 @@ const app = {
|
||||
}),
|
||||
|
||||
components: {
|
||||
'page-settings': Vue.component('page-settings'),
|
||||
'page-reports-main': Vue.component('page-reports-main'),
|
||||
'page-settings': page_settings,
|
||||
'page-reports-main': page_reports_main,
|
||||
},
|
||||
|
||||
data: {
|
||||
@ -64,12 +69,12 @@ const app = {
|
||||
|
||||
|
||||
|
||||
function init_app() {
|
||||
init_axios_interceptors();
|
||||
|
||||
init_authentication_interceptors();
|
||||
|
||||
UserSettings.load().then(settings => {
|
||||
new Vue(app).$mount('#app');
|
||||
}).catch(error => {
|
||||
alert('' + error);
|
||||
});
|
||||
}
|
||||
UserSettings.load().then(settings => {
|
||||
new Vue(app).$mount('#app');
|
||||
}).catch(error => {
|
||||
alert('' + error);
|
||||
});
|
||||
|
||||
|
@ -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({
|
||||
|
||||
template: response.data,
|
||||
|
||||
components: {
|
||||
'page-layout': Vue.component('page-layout'),
|
||||
'reports-page-header': Vue.component('reports-page-header'),
|
||||
'date-range-picker': Vue.component('date-range-picker'),
|
||||
'panel-messages-sent': Vue.component('panel-messages-sent'),
|
||||
'panel-messages-received': Vue.component('panel-messages-received'),
|
||||
'panel-flagged-connections': Vue.component('panel-flagged-connections'),
|
||||
'panel-user-activity': Vue.component('panel-user-activity'),
|
||||
'page-layout': page_layout,
|
||||
'reports-page-header': reports_page_header,
|
||||
'date-range-picker': date_range_picker,
|
||||
'capture-db-stats': capture_db_stats,
|
||||
'panel-messages-sent': panel_messages_sent,
|
||||
'panel-messages-received': panel_messages_received,
|
||||
'panel-flagged-connections': panel_flagged_connections,
|
||||
'panel-user-activity': panel_user_activity,
|
||||
'panel-remote-sender-activity': panel_remote_sender_activity,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
@ -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({
|
||||
|
||||
template: response.data,
|
||||
|
||||
components: {
|
||||
'page-layout': Vue.component('page-layout'),
|
||||
'reports-page-header': Vue.component('reports-page-header'),
|
||||
'page-layout': page_layout,
|
||||
'reports-page-header': reports_page_header,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
@ -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({
|
||||
|
||||
template: response.data,
|
||||
@ -14,10 +23,10 @@ Vue.component('panel-flagged-connections', function(resolve, reject) {
|
||||
},
|
||||
|
||||
components: {
|
||||
'chart-multi-line-timeseries': Vue.component('chart-multi-line-timeseries'),
|
||||
'chart-stacked-bar-timeseries': Vue.component('chart-stacked-bar-timeseries'),
|
||||
'chart-pie': Vue.component('chart-pie'),
|
||||
'chart-table': Vue.component('chart-table'),
|
||||
'chart-multi-line-timeseries': chart_multi_line_timeseries,
|
||||
'chart-stacked-bar-timeseries': chart_stacked_bar_timeseries,
|
||||
'chart-pie': chart_pie,
|
||||
'chart-table': chart_table,
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -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({
|
||||
|
||||
template: response.data,
|
||||
@ -19,10 +25,8 @@ Vue.component('panel-messages-received', function(resolve, reject) {
|
||||
},
|
||||
|
||||
components: {
|
||||
'chart-multi-line-timeseries': Vue.component('chart-multi-line-timeseries'),
|
||||
// 'chart-stacked-bar-timeseries': Vue.component('chart-stacked-bar-timeseries'),
|
||||
// 'chart-pie': Vue.component('chart-pie'),
|
||||
'chart-table': Vue.component('chart-table'),
|
||||
'chart-multi-line-timeseries': chart_multi_line_timeseries,
|
||||
'chart-table': chart_table,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
@ -9,7 +9,15 @@
|
||||
'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({
|
||||
|
||||
template: response.data,
|
||||
@ -27,10 +35,10 @@ Vue.component('panel-messages-sent', function(resolve, reject) {
|
||||
},
|
||||
|
||||
components: {
|
||||
'chart-multi-line-timeseries': Vue.component('chart-multi-line-timeseries'),
|
||||
'chart-stacked-bar-timeseries': Vue.component('chart-stacked-bar-timeseries'),
|
||||
'chart-pie': Vue.component('chart-pie'),
|
||||
'chart-table': Vue.component('chart-table'),
|
||||
'chart-multi-line-timeseries': chart_multi_line_timeseries,
|
||||
'chart-stacked-bar-timeseries': chart_stacked_bar_timeseries,
|
||||
'chart-pie': chart_pie,
|
||||
'chart-table': chart_table,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
@ -2,7 +2,11 @@
|
||||
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({
|
||||
|
||||
template: response.data,
|
||||
|
@ -1,5 +1,9 @@
|
||||
<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">
|
||||
<option v-for="user in all_users">{{ user }}</option>
|
||||
</datalist>
|
||||
@ -61,7 +65,7 @@
|
||||
</template>
|
||||
<template #row-details="row">
|
||||
<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 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>
|
||||
@ -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_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.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.lmtp_id"><a href="#" @click.prevent.stop="show_message_headers(row.item.lmtp_id)">Message headers</a></div>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
@ -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:'@_.,:+=' },
|
||||
|
@ -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
|
||||
|
@ -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 <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)
|
||||
|
||||
|
||||
|
@ -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 &&
|
||||
|
@ -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;
|
||||
|
@ -1,8 +1,8 @@
|
||||
Vue.component('spinner', {
|
||||
var spinner = Vue.component('spinner', {
|
||||
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({
|
||||
|
||||
props: {
|
||||
@ -17,3 +17,5 @@ Vue.component('page-header', function(resolve, reject) {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
export { spinner, header as default };
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user