summaryrefslogtreecommitdiffstats
path: root/interface/js/app/libft.js
diff options
context:
space:
mode:
Diffstat (limited to 'interface/js/app/libft.js')
-rw-r--r--interface/js/app/libft.js524
1 files changed, 524 insertions, 0 deletions
diff --git a/interface/js/app/libft.js b/interface/js/app/libft.js
new file mode 100644
index 0000000..c69f74d
--- /dev/null
+++ b/interface/js/app/libft.js
@@ -0,0 +1,524 @@
+/* global FooTable */
+
+define(["jquery", "app/common", "footable"],
+ ($, common) => {
+ "use strict";
+ const ui = {};
+
+ let pageSizeTimerId = null;
+ let pageSizeInvocationCounter = 0;
+
+ function get_compare_function(table) {
+ const compare_functions = {
+ magnitude: function (e1, e2) {
+ return Math.abs(e2.score) - Math.abs(e1.score);
+ },
+ name: function (e1, e2) {
+ return e1.name.localeCompare(e2.name);
+ },
+ score: function (e1, e2) {
+ return e2.score - e1.score;
+ }
+ };
+
+ return compare_functions[common.getSelector("selSymOrder_" + table)];
+ }
+
+ function sort_symbols(o, compare_function) {
+ return Object.keys(o)
+ .map((key) => o[key])
+ .sort(compare_function)
+ .map((e) => e.str)
+ .join("<br>\n");
+ }
+
+
+ // Public functions
+
+ ui.formatBytesIEC = function (bytes) {
+ // FooTable represents data as text even column type is "number".
+ if (!Number.isInteger(Number(bytes)) || bytes < 0) return "NaN";
+
+ const base = 1024;
+ const exponent = Math.floor(Math.log(bytes) / Math.log(base));
+
+ if (exponent > 8) return "∞";
+
+ const value = parseFloat((bytes / (base ** exponent)).toPrecision(3));
+ let unit = "BKMGTPEZY"[exponent];
+ if (exponent) unit += "iB";
+
+ return value + " " + unit;
+ };
+
+ ui.columns_v2 = function (table) {
+ return [{
+ name: "id",
+ title: "ID",
+ style: {
+ minWidth: 130,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ wordBreak: "break-all",
+ whiteSpace: "normal"
+ }
+ }, {
+ name: "ip",
+ title: "IP address",
+ breakpoints: "xs sm md",
+ style: {
+ "minWidth": "calc(7.6em + 8px)",
+ "word-break": "break-all"
+ },
+ // Normalize IPv4
+ sortValue: (ip) => ((typeof ip === "string") ? ip.split(".").map((x) => x.padStart(3, "0")).join("") : "0")
+ }, {
+ name: "sender_mime",
+ title: "[Envelope From] From",
+ breakpoints: "xs sm md",
+ style: {
+ "minWidth": 100,
+ "maxWidth": 200,
+ "word-wrap": "break-word"
+ }
+ }, {
+ name: "rcpt_mime_short",
+ title: "[Envelope To] To/Cc/Bcc",
+ breakpoints: "xs sm md",
+ filterable: false,
+ classes: "d-none d-xl-table-cell",
+ style: {
+ "minWidth": 100,
+ "maxWidth": 200,
+ "word-wrap": "break-word"
+ }
+ }, {
+ name: "rcpt_mime",
+ title: "[Envelope To] To/Cc/Bcc",
+ breakpoints: "all",
+ style: {"word-wrap": "break-word"}
+ }, {
+ name: "subject",
+ title: "Subject",
+ breakpoints: "xs sm md",
+ style: {
+ "word-break": "break-all",
+ "minWidth": 150
+ }
+ }, {
+ name: "action",
+ title: "Action",
+ style: {minwidth: 82}
+ }, {
+ name: "passthrough_module",
+ title: '<div title="The module that has set the pre-result">Pass-through module</div>',
+ breakpoints: "xs sm md"
+ }, {
+ name: "score",
+ title: "Score",
+ style: {
+ "maxWidth": 110,
+ "text-align": "right",
+ "white-space": "nowrap"
+ },
+ sortValue: function (val) { return Number(val.options.sortValue); }
+ }, {
+ name: "symbols",
+ title: "Symbols" +
+ '<div class="sym-order-toggle">' +
+ '<br><span style="font-weight:normal;">Sort by:</span><br>' +
+ '<div class="btn-group btn-group-xs btn-sym-order-' + table + '">' +
+ '<label type="button" class="btn btn-outline-secondary btn-sym-' + table + '-magnitude">' +
+ '<input type="radio" class="btn-check" value="magnitude">Magnitude</label>' +
+ '<label type="button" class="btn btn-outline-secondary btn-sym-' + table + '-score">' +
+ '<input type="radio" class="btn-check" value="score">Value</label>' +
+ '<label type="button" class="btn btn-outline-secondary btn-sym-' + table + '-name">' +
+ '<input type="radio" class="btn-check" value="name">Name</label>' +
+ "</div>" +
+ "</div>",
+ breakpoints: "all",
+ style: {width: 550, maxWidth: 550}
+ }, {
+ name: "size",
+ title: "Msg size",
+ breakpoints: "xs sm md",
+ style: {minwidth: 50},
+ formatter: ui.formatBytesIEC
+ }, {
+ name: "time_real",
+ title: "Scan time",
+ breakpoints: "xs sm md",
+ style: {maxWidth: 72},
+ sortValue: function (val) { return Number(val); }
+ }, {
+ classes: "history-col-time",
+ sorted: true,
+ direction: "DESC",
+ name: "time",
+ title: "Time",
+ sortValue: function (val) { return Number(val.options.sortValue); }
+ }, {
+ name: "user",
+ title: "Authenticated user",
+ breakpoints: "xs sm md",
+ style: {
+ "minWidth": 100,
+ "maxWidth": 130,
+ "word-wrap": "break-word"
+ }
+ }].filter((col) => {
+ switch (table) {
+ case "history":
+ return (col.name !== "passthrough_module");
+ case "scan":
+ return ["ip", "sender_mime", "rcpt_mime_short", "rcpt_mime", "subject", "size", "user"]
+ .every((name) => col.name !== name);
+ default:
+ return null;
+ }
+ });
+ };
+
+ ui.set_page_size = function (table, page_size, changeTablePageSize) {
+ const n = parseInt(page_size, 10); // HTML Input elements return string representing a number
+ if (n > 0) {
+ common.page_size[table] = n;
+
+ if (changeTablePageSize &&
+ $("#historyTable_" + table + " tbody").is(":parent")) { // Table is not empty
+ clearTimeout(pageSizeTimerId);
+ const t = FooTable.get("#historyTable_" + table);
+ if (t) {
+ pageSizeInvocationCounter = 0;
+ // Wait for input finish
+ pageSizeTimerId = setTimeout(() => t.pageSize(n), 1000);
+ } else if (++pageSizeInvocationCounter < 10) {
+ // Wait for FooTable instance ready
+ pageSizeTimerId = setTimeout(() => ui.set_page_size(table, n, true), 1000);
+ }
+ }
+ }
+ };
+
+ ui.bindHistoryTableEventHandlers = function (table, symbolsCol) {
+ function change_symbols_order(order) {
+ $(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active");
+ const compare_function = get_compare_function(table);
+ $.each(common.tables[table].rows.all, (i, row) => {
+ const cell_val = sort_symbols(common.symbols[table][i], compare_function);
+ row.cells[symbolsCol].val(cell_val, false, true);
+ });
+ }
+
+ $("#selSymOrder_" + table).unbind().change(function () {
+ const order = this.value;
+ change_symbols_order(order);
+ });
+ $("#" + table + "_page_size").change((e) => ui.set_page_size(table, e.target.value, true));
+ $(document).on("click", ".btn-sym-order-" + table + " input", function () {
+ const order = this.value;
+ $("#selSymOrder_" + table).val(order);
+ change_symbols_order(order);
+ });
+ };
+
+ ui.destroyTable = function (table) {
+ if (common.tables[table]) {
+ common.tables[table].destroy();
+ delete common.tables[table];
+ }
+ };
+
+ ui.initHistoryTable = function (data, items, table, columns, expandFirst) {
+ /* eslint-disable no-underscore-dangle */
+ FooTable.Cell.extend("collapse", function () {
+ // call the original method
+ this._super();
+ // Copy cell classes to detail row tr element
+ this._setClasses(this.$detail);
+ });
+ /* eslint-enable no-underscore-dangle */
+
+ /* eslint-disable consistent-this, no-underscore-dangle, one-var-declaration-per-line */
+ FooTable.actionFilter = FooTable.Filtering.extend({
+ construct: function (instance) {
+ this._super(instance);
+ this.actions = ["reject", "add header", "greylist",
+ "no action", "soft reject", "rewrite subject"];
+ this.def = "Any action";
+ this.$action = null;
+ },
+ $create: function () {
+ this._super();
+ const self = this;
+ const $form_grp = $("<div/>", {
+ class: "form-group d-inline-flex align-items-center"
+ }).append($("<label/>", {
+ class: "sr-only",
+ text: "Action"
+ })).prependTo(self.$form);
+
+ $("<div/>", {
+ class: "form-check form-check-inline",
+ title: "Invert action match."
+ }).append(
+ self.$not = $("<input/>", {
+ type: "checkbox",
+ class: "form-check-input",
+ id: "not_" + table
+ }).on("change", {self: self}, self._onStatusDropdownChanged),
+ $("<label/>", {
+ class: "form-check-label",
+ for: "not_" + table,
+ text: "not"
+ })
+ ).appendTo($form_grp);
+
+ self.$action = $("<select/>", {
+ class: "form-select"
+ }).on("change", {
+ self: self
+ }, self._onStatusDropdownChanged).append(
+ $("<option/>", {
+ text: self.def
+ })).appendTo($form_grp);
+
+ $.each(self.actions, (i, action) => {
+ self.$action.append($("<option/>").text(action));
+ });
+ },
+ _onStatusDropdownChanged: function (e) {
+ const {self} = e.data;
+ const selected = self.$action.val();
+ if (selected !== self.def) {
+ const not = self.$not.is(":checked");
+ let query = null;
+
+ if (selected === "reject") {
+ query = not ? "-reject OR soft" : "reject -soft";
+ } else {
+ query = not ? selected.replace(/(\b\w+\b)/g, "-$1") : selected;
+ }
+
+ self.addFilter("action", query, ["action"]);
+ } else {
+ self.removeFilter("action");
+ }
+ self.filter();
+ }
+ });
+ /* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */
+
+ common.tables[table] = FooTable.init("#historyTable_" + table, {
+ columns: columns,
+ rows: items,
+ expandFirst: expandFirst,
+ paging: {
+ enabled: true,
+ limit: 5,
+ size: common.page_size[table]
+ },
+ filtering: {
+ enabled: true,
+ position: "left",
+ connectors: false
+ },
+ sorting: {
+ enabled: true
+ },
+ components: {
+ filtering: FooTable.actionFilter
+ },
+ on: {
+ "expand.ft.row": function (e, ft, row) {
+ setTimeout(() => {
+ const detail_row = row.$el.next();
+ const order = common.getSelector("selSymOrder_" + table);
+ detail_row.find(".btn-sym-" + table + "-" + order)
+ .addClass("active").siblings().removeClass("active");
+ }, 5);
+ }
+ }
+ });
+ };
+
+ ui.preprocess_item = function (item) {
+ function escape_HTML_array(arr) {
+ arr.forEach((d, i) => { arr[i] = common.escapeHTML(d); });
+ }
+
+ for (const prop in item) {
+ if (!{}.hasOwnProperty.call(item, prop)) continue;
+ switch (prop) {
+ case "rcpt_mime":
+ case "rcpt_smtp":
+ escape_HTML_array(item[prop]);
+ break;
+ case "symbols":
+ Object.keys(item.symbols).forEach((key) => {
+ const sym = item.symbols[key];
+ if (!sym.name) {
+ sym.name = key;
+ }
+ sym.name = common.escapeHTML(sym.name);
+ if (sym.description) {
+ sym.description = common.escapeHTML(sym.description);
+ }
+
+ if (sym.options) {
+ escape_HTML_array(sym.options);
+ }
+ });
+ break;
+ default:
+ if (typeof item[prop] === "string") {
+ item[prop] = common.escapeHTML(item[prop]);
+ }
+ }
+ }
+
+ if (item.action === "clean" || item.action === "no action") {
+ item.action = "<div style='font-size:11px' class='badge text-bg-success'>" + item.action + "</div>";
+ } else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") {
+ item.action = "<div style='font-size:11px' class='badge text-bg-warning'>" + item.action + "</div>";
+ } else if (item.action === "spam" || item.action === "reject") {
+ item.action = "<div style='font-size:11px' class='badge text-bg-danger'>" + item.action + "</div>";
+ } else {
+ item.action = "<div style='font-size:11px' class='badge text-bg-info'>" + item.action + "</div>";
+ }
+
+ const score_content = (item.score < item.required_score)
+ ? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"
+ : "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>";
+
+ item.score = {
+ options: {
+ sortValue: item.score
+ },
+ value: score_content
+ };
+ };
+
+ ui.unix_time_format = function (tm) {
+ const date = new Date(tm ? tm * 1000 : 0);
+ return (common.locale)
+ ? date.toLocaleString(common.locale)
+ : date.toLocaleString();
+ };
+
+ ui.process_history_v2 = function (data, table) {
+ // Display no more than rcpt_lim recipients
+ const rcpt_lim = 3;
+ const items = [];
+ const unsorted_symbols = [];
+ const compare_function = get_compare_function(table);
+
+ $("#selSymOrder_" + table + ", label[for='selSymOrder_" + table + "']").show();
+
+ $.each(data.rows,
+ (i, item) => {
+ function more(p) {
+ const l = item[p].length;
+ return (l > rcpt_lim) ? " … (" + l + ")" : "";
+ }
+ function format_rcpt(smtp, mime) {
+ let full = "";
+ let shrt = "";
+ if (smtp) {
+ full = "[" + item.rcpt_smtp.join(", ") + "] ";
+ shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_smtp") + "]";
+ if (mime) {
+ full += " ";
+ shrt += " ";
+ }
+ }
+ if (mime) {
+ full += item.rcpt_mime.join(", ");
+ shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_mime");
+ }
+ return {full: full, shrt: shrt};
+ }
+
+ function get_symbol_class(name, score) {
+ if (name.match(/^GREYLIST$/)) {
+ return "symbol-special";
+ }
+
+ if (score < 0) {
+ return "symbol-negative";
+ } else if (score > 0) {
+ return "symbol-positive";
+ }
+ return null;
+ }
+
+ ui.preprocess_item(item);
+ Object.values(item.symbols).forEach((sym) => {
+ sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>';
+
+ if (sym.description) {
+ sym.str += '<abbr title="' + sym.description + '">' + sym.name + "</abbr>";
+ } else {
+ sym.str += sym.name;
+ }
+ sym.str += "</strong> (" + sym.score + ")</span>";
+
+ if (sym.options) {
+ sym.str += " [" + sym.options.join(",") + "]";
+ }
+ });
+ unsorted_symbols.push(item.symbols);
+ item.symbols = sort_symbols(item.symbols, compare_function);
+ if (table === "scan") {
+ item.unix_time = (new Date()).getTime() / 1000;
+ }
+ item.time = {
+ value: ui.unix_time_format(item.unix_time),
+ options: {
+ sortValue: item.unix_time
+ }
+ };
+ item.time_real = item.time_real.toFixed(3);
+ item.id = item["message-id"];
+
+ if (table === "history") {
+ let rcpt = {};
+ if (!item.rcpt_mime.length) {
+ rcpt = format_rcpt(true, false);
+ } else if (
+ $(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 ||
+ $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0
+ ) {
+ rcpt = format_rcpt(true, true);
+ } else {
+ rcpt = format_rcpt(false, true);
+ }
+ item.rcpt_mime_short = rcpt.shrt;
+ item.rcpt_mime = rcpt.full;
+
+ if (item.sender_mime !== item.sender_smtp) {
+ item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime;
+ }
+ }
+ items.push(item);
+ });
+
+ return {items: items, symbols: unsorted_symbols};
+ };
+
+ ui.waitForRowsDisplayed = function (table, rows_total, callback, iteration) {
+ let i = (typeof iteration === "undefined") ? 10 : iteration;
+ const num_rows = $("#historyTable_" + table + " > tbody > tr:not(.footable-detail-row)").length;
+ if (num_rows === common.page_size[table] ||
+ num_rows === rows_total) {
+ return callback();
+ } else if (--i) {
+ setTimeout(() => {
+ ui.waitForRowsDisplayed(table, rows_total, callback, i);
+ }, 500);
+ }
+ return null;
+ };
+
+ return ui;
+ });