You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. /* global FooTable */
  2. define(["jquery", "app/common", "footable"],
  3. ($, common) => {
  4. "use strict";
  5. const ui = {};
  6. let pageSizeTimerId = null;
  7. let pageSizeInvocationCounter = 0;
  8. function get_compare_function(table) {
  9. const compare_functions = {
  10. magnitude: function (e1, e2) {
  11. return Math.abs(e2.score) - Math.abs(e1.score);
  12. },
  13. name: function (e1, e2) {
  14. return e1.name.localeCompare(e2.name);
  15. },
  16. score: function (e1, e2) {
  17. return e2.score - e1.score;
  18. }
  19. };
  20. return compare_functions[common.getSelector("selSymOrder_" + table)];
  21. }
  22. function sort_symbols(o, compare_function) {
  23. return Object.keys(o)
  24. .map((key) => o[key])
  25. .sort(compare_function)
  26. .map((e) => e.str)
  27. .join("<br>\n");
  28. }
  29. // Public functions
  30. ui.formatBytesIEC = function (bytes) {
  31. // FooTable represents data as text even column type is "number".
  32. if (!Number.isInteger(Number(bytes)) || bytes < 0) return "NaN";
  33. const base = 1024;
  34. const exponent = Math.floor(Math.log(bytes) / Math.log(base));
  35. if (exponent > 8) return "∞";
  36. const value = parseFloat((bytes / (base ** exponent)).toPrecision(3));
  37. let unit = "BKMGTPEZY"[exponent];
  38. if (exponent) unit += "iB";
  39. return value + " " + unit;
  40. };
  41. ui.columns_v2 = function (table) {
  42. return [{
  43. name: "id",
  44. title: "ID",
  45. style: {
  46. minWidth: 130,
  47. overflow: "hidden",
  48. textOverflow: "ellipsis",
  49. wordBreak: "break-all",
  50. whiteSpace: "normal"
  51. }
  52. }, {
  53. name: "ip",
  54. title: "IP address",
  55. breakpoints: "xs sm md",
  56. style: {
  57. "minWidth": "calc(7.6em + 8px)",
  58. "word-break": "break-all"
  59. },
  60. // Normalize IPv4
  61. sortValue: (ip) => ((typeof ip === "string") ? ip.split(".").map((x) => x.padStart(3, "0")).join("") : "0")
  62. }, {
  63. name: "sender_mime",
  64. title: "[Envelope From] From",
  65. breakpoints: "xs sm md",
  66. style: {
  67. "minWidth": 100,
  68. "maxWidth": 200,
  69. "word-wrap": "break-word"
  70. }
  71. }, {
  72. name: "rcpt_mime_short",
  73. title: "[Envelope To] To/Cc/Bcc",
  74. breakpoints: "xs sm md",
  75. filterable: false,
  76. classes: "d-none d-xl-table-cell",
  77. style: {
  78. "minWidth": 100,
  79. "maxWidth": 200,
  80. "word-wrap": "break-word"
  81. }
  82. }, {
  83. name: "rcpt_mime",
  84. title: "[Envelope To] To/Cc/Bcc",
  85. breakpoints: "all",
  86. style: {"word-wrap": "break-word"}
  87. }, {
  88. name: "subject",
  89. title: "Subject",
  90. breakpoints: "xs sm md",
  91. style: {
  92. "word-break": "break-all",
  93. "minWidth": 150
  94. }
  95. }, {
  96. name: "action",
  97. title: "Action",
  98. style: {minwidth: 82}
  99. }, {
  100. name: "passthrough_module",
  101. title: '<div title="The module that has set the pre-result">Pass-through module</div>',
  102. breakpoints: "xs sm md"
  103. }, {
  104. name: "score",
  105. title: "Score",
  106. style: {
  107. "maxWidth": 110,
  108. "text-align": "right",
  109. "white-space": "nowrap"
  110. },
  111. sortValue: function (val) { return Number(val.options.sortValue); }
  112. }, {
  113. name: "symbols",
  114. title: "Symbols" +
  115. '<div class="sym-order-toggle">' +
  116. '<br><span style="font-weight:normal;">Sort by:</span><br>' +
  117. '<div class="btn-group btn-group-xs btn-sym-order-' + table + '">' +
  118. '<label type="button" class="btn btn-outline-secondary btn-sym-' + table + '-magnitude">' +
  119. '<input type="radio" class="btn-check" value="magnitude">Magnitude</label>' +
  120. '<label type="button" class="btn btn-outline-secondary btn-sym-' + table + '-score">' +
  121. '<input type="radio" class="btn-check" value="score">Value</label>' +
  122. '<label type="button" class="btn btn-outline-secondary btn-sym-' + table + '-name">' +
  123. '<input type="radio" class="btn-check" value="name">Name</label>' +
  124. "</div>" +
  125. "</div>",
  126. breakpoints: "all",
  127. style: {width: 550, maxWidth: 550}
  128. }, {
  129. name: "size",
  130. title: "Msg size",
  131. breakpoints: "xs sm md",
  132. style: {minwidth: 50},
  133. formatter: ui.formatBytesIEC
  134. }, {
  135. name: "time_real",
  136. title: "Scan time",
  137. breakpoints: "xs sm md",
  138. style: {maxWidth: 72},
  139. sortValue: function (val) { return Number(val); }
  140. }, {
  141. classes: "history-col-time",
  142. sorted: true,
  143. direction: "DESC",
  144. name: "time",
  145. title: "Time",
  146. sortValue: function (val) { return Number(val.options.sortValue); }
  147. }, {
  148. name: "user",
  149. title: "Authenticated user",
  150. breakpoints: "xs sm md",
  151. style: {
  152. "minWidth": 100,
  153. "maxWidth": 130,
  154. "word-wrap": "break-word"
  155. }
  156. }].filter((col) => {
  157. switch (table) {
  158. case "history":
  159. return (col.name !== "passthrough_module");
  160. case "scan":
  161. return ["ip", "sender_mime", "rcpt_mime_short", "rcpt_mime", "subject", "size", "user"]
  162. .every((name) => col.name !== name);
  163. default:
  164. return null;
  165. }
  166. });
  167. };
  168. ui.set_page_size = function (table, page_size, changeTablePageSize) {
  169. const n = parseInt(page_size, 10); // HTML Input elements return string representing a number
  170. if (n > 0) {
  171. common.page_size[table] = n;
  172. if (changeTablePageSize &&
  173. $("#historyTable_" + table + " tbody").is(":parent")) { // Table is not empty
  174. clearTimeout(pageSizeTimerId);
  175. const t = FooTable.get("#historyTable_" + table);
  176. if (t) {
  177. pageSizeInvocationCounter = 0;
  178. // Wait for input finish
  179. pageSizeTimerId = setTimeout(() => t.pageSize(n), 1000);
  180. } else if (++pageSizeInvocationCounter < 10) {
  181. // Wait for FooTable instance ready
  182. pageSizeTimerId = setTimeout(() => ui.set_page_size(table, n, true), 1000);
  183. }
  184. }
  185. }
  186. };
  187. ui.bindHistoryTableEventHandlers = function (table, symbolsCol) {
  188. function change_symbols_order(order) {
  189. $(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active");
  190. const compare_function = get_compare_function(table);
  191. $.each(common.tables[table].rows.all, (i, row) => {
  192. const cell_val = sort_symbols(common.symbols[table][i], compare_function);
  193. row.cells[symbolsCol].val(cell_val, false, true);
  194. });
  195. }
  196. $("#selSymOrder_" + table).unbind().change(function () {
  197. const order = this.value;
  198. change_symbols_order(order);
  199. });
  200. $("#" + table + "_page_size").change((e) => ui.set_page_size(table, e.target.value, true));
  201. $(document).on("click", ".btn-sym-order-" + table + " input", function () {
  202. const order = this.value;
  203. $("#selSymOrder_" + table).val(order);
  204. change_symbols_order(order);
  205. });
  206. };
  207. ui.destroyTable = function (table) {
  208. if (common.tables[table]) {
  209. common.tables[table].destroy();
  210. delete common.tables[table];
  211. }
  212. };
  213. ui.initHistoryTable = function (data, items, table, columns, expandFirst) {
  214. /* eslint-disable no-underscore-dangle */
  215. FooTable.Cell.extend("collapse", function () {
  216. // call the original method
  217. this._super();
  218. // Copy cell classes to detail row tr element
  219. this._setClasses(this.$detail);
  220. });
  221. /* eslint-enable no-underscore-dangle */
  222. /* eslint-disable consistent-this, no-underscore-dangle, one-var-declaration-per-line */
  223. FooTable.actionFilter = FooTable.Filtering.extend({
  224. construct: function (instance) {
  225. this._super(instance);
  226. this.actions = ["reject", "add header", "greylist",
  227. "no action", "soft reject", "rewrite subject"];
  228. this.def = "Any action";
  229. this.$action = null;
  230. },
  231. $create: function () {
  232. this._super();
  233. const self = this;
  234. const $form_grp = $("<div/>", {
  235. class: "form-group d-inline-flex align-items-center"
  236. }).append($("<label/>", {
  237. class: "sr-only",
  238. text: "Action"
  239. })).prependTo(self.$form);
  240. $("<div/>", {
  241. class: "form-check form-check-inline",
  242. title: "Invert action match."
  243. }).append(
  244. self.$not = $("<input/>", {
  245. type: "checkbox",
  246. class: "form-check-input",
  247. id: "not_" + table
  248. }).on("change", {self: self}, self._onStatusDropdownChanged),
  249. $("<label/>", {
  250. class: "form-check-label",
  251. for: "not_" + table,
  252. text: "not"
  253. })
  254. ).appendTo($form_grp);
  255. self.$action = $("<select/>", {
  256. class: "form-select"
  257. }).on("change", {
  258. self: self
  259. }, self._onStatusDropdownChanged).append(
  260. $("<option/>", {
  261. text: self.def
  262. })).appendTo($form_grp);
  263. $.each(self.actions, (i, action) => {
  264. self.$action.append($("<option/>").text(action));
  265. });
  266. },
  267. _onStatusDropdownChanged: function (e) {
  268. const {self} = e.data;
  269. const selected = self.$action.val();
  270. if (selected !== self.def) {
  271. const not = self.$not.is(":checked");
  272. let query = null;
  273. if (selected === "reject") {
  274. query = not ? "-reject OR soft" : "reject -soft";
  275. } else {
  276. query = not ? selected.replace(/(\b\w+\b)/g, "-$1") : selected;
  277. }
  278. self.addFilter("action", query, ["action"]);
  279. } else {
  280. self.removeFilter("action");
  281. }
  282. self.filter();
  283. }
  284. });
  285. /* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */
  286. common.tables[table] = FooTable.init("#historyTable_" + table, {
  287. columns: columns,
  288. rows: items,
  289. expandFirst: expandFirst,
  290. paging: {
  291. enabled: true,
  292. limit: 5,
  293. size: common.page_size[table]
  294. },
  295. filtering: {
  296. enabled: true,
  297. position: "left",
  298. connectors: false
  299. },
  300. sorting: {
  301. enabled: true
  302. },
  303. components: {
  304. filtering: FooTable.actionFilter
  305. },
  306. on: {
  307. "expand.ft.row": function (e, ft, row) {
  308. setTimeout(() => {
  309. const detail_row = row.$el.next();
  310. const order = common.getSelector("selSymOrder_" + table);
  311. detail_row.find(".btn-sym-" + table + "-" + order)
  312. .addClass("active").siblings().removeClass("active");
  313. }, 5);
  314. }
  315. }
  316. });
  317. };
  318. ui.preprocess_item = function (item) {
  319. function escape_HTML_array(arr) {
  320. arr.forEach((d, i) => { arr[i] = common.escapeHTML(d); });
  321. }
  322. for (const prop in item) {
  323. if (!{}.hasOwnProperty.call(item, prop)) continue;
  324. switch (prop) {
  325. case "rcpt_mime":
  326. case "rcpt_smtp":
  327. escape_HTML_array(item[prop]);
  328. break;
  329. case "symbols":
  330. Object.keys(item.symbols).forEach((key) => {
  331. const sym = item.symbols[key];
  332. if (!sym.name) {
  333. sym.name = key;
  334. }
  335. sym.name = common.escapeHTML(sym.name);
  336. if (sym.description) {
  337. sym.description = common.escapeHTML(sym.description);
  338. }
  339. if (sym.options) {
  340. escape_HTML_array(sym.options);
  341. }
  342. });
  343. break;
  344. default:
  345. if (typeof item[prop] === "string") {
  346. item[prop] = common.escapeHTML(item[prop]);
  347. }
  348. }
  349. }
  350. if (item.action === "clean" || item.action === "no action") {
  351. item.action = "<div style='font-size:11px' class='badge text-bg-success'>" + item.action + "</div>";
  352. } else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") {
  353. item.action = "<div style='font-size:11px' class='badge text-bg-warning'>" + item.action + "</div>";
  354. } else if (item.action === "spam" || item.action === "reject") {
  355. item.action = "<div style='font-size:11px' class='badge text-bg-danger'>" + item.action + "</div>";
  356. } else {
  357. item.action = "<div style='font-size:11px' class='badge text-bg-info'>" + item.action + "</div>";
  358. }
  359. const score_content = (item.score < item.required_score)
  360. ? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"
  361. : "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>";
  362. item.score = {
  363. options: {
  364. sortValue: item.score
  365. },
  366. value: score_content
  367. };
  368. };
  369. ui.unix_time_format = function (tm) {
  370. const date = new Date(tm ? tm * 1000 : 0);
  371. return (common.locale)
  372. ? date.toLocaleString(common.locale)
  373. : date.toLocaleString();
  374. };
  375. ui.process_history_v2 = function (data, table) {
  376. // Display no more than rcpt_lim recipients
  377. const rcpt_lim = 3;
  378. const items = [];
  379. const unsorted_symbols = [];
  380. const compare_function = get_compare_function(table);
  381. $("#selSymOrder_" + table + ", label[for='selSymOrder_" + table + "']").show();
  382. $.each(data.rows,
  383. (i, item) => {
  384. function more(p) {
  385. const l = item[p].length;
  386. return (l > rcpt_lim) ? " … (" + l + ")" : "";
  387. }
  388. function format_rcpt(smtp, mime) {
  389. let full = "";
  390. let shrt = "";
  391. if (smtp) {
  392. full = "[" + item.rcpt_smtp.join(", ") + "] ";
  393. shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_smtp") + "]";
  394. if (mime) {
  395. full += " ";
  396. shrt += " ";
  397. }
  398. }
  399. if (mime) {
  400. full += item.rcpt_mime.join(", ");
  401. shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_mime");
  402. }
  403. return {full: full, shrt: shrt};
  404. }
  405. function get_symbol_class(name, score) {
  406. if (name.match(/^GREYLIST$/)) {
  407. return "symbol-special";
  408. }
  409. if (score < 0) {
  410. return "symbol-negative";
  411. } else if (score > 0) {
  412. return "symbol-positive";
  413. }
  414. return null;
  415. }
  416. ui.preprocess_item(item);
  417. Object.values(item.symbols).forEach((sym) => {
  418. sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>';
  419. if (sym.description) {
  420. sym.str += '<abbr title="' + sym.description + '">' + sym.name + "</abbr>";
  421. } else {
  422. sym.str += sym.name;
  423. }
  424. sym.str += "</strong> (" + sym.score + ")</span>";
  425. if (sym.options) {
  426. sym.str += " [" + sym.options.join(",") + "]";
  427. }
  428. });
  429. unsorted_symbols.push(item.symbols);
  430. item.symbols = sort_symbols(item.symbols, compare_function);
  431. if (table === "scan") {
  432. item.unix_time = (new Date()).getTime() / 1000;
  433. }
  434. item.time = {
  435. value: ui.unix_time_format(item.unix_time),
  436. options: {
  437. sortValue: item.unix_time
  438. }
  439. };
  440. item.time_real = item.time_real.toFixed(3);
  441. item.id = item["message-id"];
  442. if (table === "history") {
  443. let rcpt = {};
  444. if (!item.rcpt_mime.length) {
  445. rcpt = format_rcpt(true, false);
  446. } else if (
  447. $(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 ||
  448. $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0
  449. ) {
  450. rcpt = format_rcpt(true, true);
  451. } else {
  452. rcpt = format_rcpt(false, true);
  453. }
  454. item.rcpt_mime_short = rcpt.shrt;
  455. item.rcpt_mime = rcpt.full;
  456. if (item.sender_mime !== item.sender_smtp) {
  457. item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime;
  458. }
  459. }
  460. items.push(item);
  461. });
  462. return {items: items, symbols: unsorted_symbols};
  463. };
  464. ui.waitForRowsDisplayed = function (table, rows_total, callback, iteration) {
  465. let i = (typeof iteration === "undefined") ? 10 : iteration;
  466. const num_rows = $("#historyTable_" + table + " > tbody > tr:not(.footable-detail-row)").length;
  467. if (num_rows === common.page_size[table] ||
  468. num_rows === rows_total) {
  469. return callback();
  470. } else if (--i) {
  471. setTimeout(() => {
  472. ui.waitForRowsDisplayed(table, rows_total, callback, i);
  473. }, 500);
  474. }
  475. return null;
  476. };
  477. return ui;
  478. });