blob: 85e6b7895d4151c0a6715b28a6b958928f7e9c71 [file] [log] [blame]
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001const LOCALE = undefined;
2const DATETIME_FORMAT = {
3 year: "numeric",
4 month: "2-digit",
5 day: "2-digit",
6 hour: "2-digit",
7 minute: "2-digit",
8 second: "2-digit"
9};
10
11$(document).ready(function() {
12 // Parse seconds ago to date
13 // Get "now" timestamp
14 var ts_now = Math.round((new Date()).getTime() / 1000);
15 $('.parse_s_ago').each(function(i, parse_s_ago) {
16 var started_s_ago = parseInt($(this).text(), 10);
17 if (typeof started_s_ago != 'NaN') {
18 var started_date = new Date((ts_now - started_s_ago) * 1000);
19 if (started_date instanceof Date && !isNaN(started_date)) {
20 var started_local_date = started_date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
21 $(this).text(started_local_date);
22 } else {
23 $(this).text('-');
24 }
25 }
26 });
27 // Parse general dates
28 $('.parse_date').each(function(i, parse_date) {
29 var started_date = new Date(Date.parse($(this).text()));
30 if (typeof started_date != 'NaN') {
31 var started_local_date = started_date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
32 $(this).text(started_local_date);
33 }
34 });
35
36 // set update loop container list
37 containersToUpdate = {}
38 // set default ChartJs Font Color
39 Chart.defaults.color = '#999';
40 // create host cpu and mem charts
41 createHostCpuAndMemChart();
42 // check for new version
43 if (mailcow_info.branch === "master"){
44 check_update(mailcow_info.version_tag, mailcow_info.project_url);
45 }
46 $("#maiclow_version").click(function(){
47 if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" ||
48 mailcow_info.branch !== "master")
49 return;
50
51 showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
52 })
53 // get public ips
54 get_public_ips();
55 update_container_stats();
56});
57jQuery(function($){
58 if (localStorage.getItem("current_page") === null) {
59 var current_page = {};
60 } else {
61 var current_page = JSON.parse(localStorage.getItem('current_page'));
62 }
63 // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
64 var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
65 function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
66 function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
67 function hashCode(t){for(var n=0,r=0;r<t.length;r++)n=t.charCodeAt(r)+((n<<5)-n);return n}
68 function intToRGB(t){var n=(16777215&t).toString(16).toUpperCase();return"00000".substring(0,6-n.length)+n}
69 $(".refresh_table").on('click', function(e) {
70 e.preventDefault();
71 var table_name = $(this).data('table');
72 $('#' + table_name).DataTable().ajax.reload();
73 });
74 function createSortableDate(td, cellData) {
75 $(td).attr({
76 "data-order": cellData,
77 "data-sort": cellData
78 });
79 $(td).html(convertTimestampToLocalFormat(cellData));
80 }
81 function draw_autodiscover_logs() {
82 // just recalc width if instance already exists
83 if ($.fn.DataTable.isDataTable('#autodiscover_log') ) {
84 $('#autodiscover_log').DataTable().columns.adjust().responsive.recalc();
85 return;
86 }
87
88 $('#autodiscover_log').DataTable({
89 processing: true,
90 serverSide: false,
91 language: lang_datatables,
92 order: [[0, 'desc']],
93 ajax: {
94 type: "GET",
95 url: "/api/v1/get/logs/autodiscover/100",
96 dataSrc: function(data){
97 return process_table_data(data, 'autodiscover_log');
98 }
99 },
100 columns: [
101 {
102 title: lang.time,
103 data: 'time',
104 defaultContent: '',
105 responsivePriority: 1,
106 createdCell: function(td, cellData) {
107 createSortableDate(td, cellData)
108 }
109 },
110 {
111 title: 'User-Agent',
112 data: 'ua',
113 defaultContent: '',
114 className: 'dtr-col-md',
115 responsivePriority: 5
116 },
117 {
118 title: 'Username',
119 data: 'user',
120 defaultContent: '',
121 responsivePriority: 4
122 },
123 {
124 title: 'IP',
125 data: 'ip',
126 defaultContent: '',
127 responsivePriority: 2
128 },
129 {
130 title: 'Service',
131 data: 'service',
132 defaultContent: '',
133 responsivePriority: 3
134 }
135 ]
136 });
137 }
138 function draw_postfix_logs() {
139 // just recalc width if instance already exists
140 if ($.fn.DataTable.isDataTable('#postfix_log') ) {
141 $('#postfix_log').DataTable().columns.adjust().responsive.recalc();
142 return;
143 }
144
145 $('#postfix_log').DataTable({
146 processing: true,
147 serverSide: false,
148 language: lang_datatables,
149 order: [[0, 'desc']],
150 ajax: {
151 type: "GET",
152 url: "/api/v1/get/logs/postfix",
153 dataSrc: function(data){
154 return process_table_data(data, 'general_syslog');
155 }
156 },
157 columns: [
158 {
159 title: lang.time,
160 data: 'time',
161 defaultContent: '',
162 createdCell: function(td, cellData) {
163 createSortableDate(td, cellData)
164 }
165 },
166 {
167 title: lang.priority,
168 data: 'priority',
169 defaultContent: ''
170 },
171 {
172 title: lang.message,
173 data: 'message',
174 defaultContent: '',
175 className: 'dtr-col-md text-break'
176 }
177 ]
178 });
179 }
180 function draw_watchdog_logs() {
181 // just recalc width if instance already exists
182 if ($.fn.DataTable.isDataTable('#watchdog_log') ) {
183 $('#watchdog_log').DataTable().columns.adjust().responsive.recalc();
184 return;
185 }
186
187 $('#watchdog_log').DataTable({
188 processing: true,
189 serverSide: false,
190 language: lang_datatables,
191 order: [[0, 'desc']],
192 ajax: {
193 type: "GET",
194 url: "/api/v1/get/logs/watchdog",
195 dataSrc: function(data){
196 return process_table_data(data, 'watchdog');
197 }
198 },
199 columns: [
200 {
201 title: lang.time,
202 data: 'time',
203 defaultContent: '',
204 createdCell: function(td, cellData) {
205 createSortableDate(td, cellData)
206 }
207 },
208 {
209 title: 'Service',
210 data: 'service',
211 defaultContent: ''
212 },
213 {
214 title: 'Trend',
215 data: 'trend',
216 defaultContent: ''
217 },
218 {
219 title: lang.message,
220 data: 'message',
221 defaultContent: ''
222 }
223 ]
224 });
225 }
226 function draw_api_logs() {
227 // just recalc width if instance already exists
228 if ($.fn.DataTable.isDataTable('#api_log') ) {
229 $('#api_log').DataTable().columns.adjust().responsive.recalc();
230 return;
231 }
232
233 $('#api_log').DataTable({
234 processing: true,
235 serverSide: false,
236 language: lang_datatables,
237 order: [[0, 'desc']],
238 ajax: {
239 type: "GET",
240 url: "/api/v1/get/logs/api",
241 dataSrc: function(data){
242 return process_table_data(data, 'apilog');
243 }
244 },
245 columns: [
246 {
247 title: lang.time,
248 data: 'time',
249 defaultContent: '',
250 createdCell: function(td, cellData) {
251 createSortableDate(td, cellData)
252 }
253 },
254 {
255 title: 'URI',
256 data: 'uri',
257 defaultContent: '',
258 className: 'dtr-col-md dtr-break-all'
259 },
260 {
261 title: 'Method',
262 data: 'method',
263 defaultContent: ''
264 },
265 {
266 title: 'IP',
267 data: 'remote',
268 defaultContent: ''
269 },
270 {
271 title: 'Data',
272 data: 'data',
273 defaultContent: '',
274 className: 'dtr-col-md dtr-break-all'
275 }
276 ]
277 });
278 }
279 function draw_rl_logs() {
280 // just recalc width if instance already exists
281 if ($.fn.DataTable.isDataTable('#rl_log') ) {
282 $('#rl_log').DataTable().columns.adjust().responsive.recalc();
283 return;
284 }
285
286 $('#rl_log').DataTable({
287 processing: true,
288 serverSide: false,
289 language: lang_datatables,
290 order: [[0, 'desc']],
291 ajax: {
292 type: "GET",
293 url: "/api/v1/get/logs/ratelimited",
294 dataSrc: function(data){
295 return process_table_data(data, 'rllog');
296 }
297 },
298 columns: [
299 {
300 title: ' ',
301 data: 'indicator',
302 defaultContent: ''
303 },
304 {
305 title: lang.time,
306 data: 'time',
307 defaultContent: '',
308 createdCell: function(td, cellData) {
309 createSortableDate(td, cellData)
310 }
311 },
312 {
313 title: lang.rate_name,
314 data: 'rl_name',
315 defaultContent: ''
316 },
317 {
318 title: lang.sender,
319 data: 'from',
320 defaultContent: ''
321 },
322 {
323 title: lang.recipients,
324 data: 'rcpt',
325 defaultContent: ''
326 },
327 {
328 title: lang.authed_user,
329 data: 'user',
330 defaultContent: ''
331 },
332 {
333 title: 'Msg ID',
334 data: 'message_id',
335 defaultContent: ''
336 },
337 {
338 title: 'Header From',
339 data: 'header_from',
340 defaultContent: ''
341 },
342 {
343 title: 'Subject',
344 data: 'header_subject',
345 defaultContent: ''
346 },
347 {
348 title: 'Hash',
349 data: 'rl_hash',
350 defaultContent: ''
351 },
352 {
353 title: 'Rspamd QID',
354 data: 'qid',
355 defaultContent: ''
356 },
357 {
358 title: 'IP',
359 data: 'ip',
360 defaultContent: ''
361 },
362 {
363 title: lang.action,
364 data: 'action',
365 defaultContent: ''
366 }
367 ]
368 });
369 }
370 function draw_ui_logs() {
371 // just recalc width if instance already exists
372 if ($.fn.DataTable.isDataTable('#ui_logs') ) {
373 $('#ui_logs').DataTable().columns.adjust().responsive.recalc();
374 return;
375 }
376
377 $('#ui_logs').DataTable({
378 processing: true,
379 serverSide: false,
380 language: lang_datatables,
381 order: [[0, 'desc']],
382 ajax: {
383 type: "GET",
384 url: "/api/v1/get/logs/ui",
385 dataSrc: function(data){
386 return process_table_data(data, 'mailcow_ui');
387 }
388 },
389 columns: [
390 {
391 title: lang.time,
392 data: 'time',
393 defaultContent: '',
394 createdCell: function(td, cellData) {
395 createSortableDate(td, cellData)
396 }
397 },
398 {
399 title: 'Type',
400 data: 'type',
401 defaultContent: ''
402 },
403 {
404 title: 'Task',
405 data: 'task',
406 defaultContent: ''
407 },
408 {
409 title: 'User',
410 data: 'user',
411 defaultContent: '',
412 className: 'dtr-col-sm'
413 },
414 {
415 title: 'Role',
416 data: 'role',
417 defaultContent: '',
418 className: 'dtr-col-sm'
419 },
420 {
421 title: 'IP',
422 data: 'remote',
423 defaultContent: '',
424 className: 'dtr-col-md dtr-break-all'
425 },
426 {
427 title: lang.message,
428 data: 'msg',
429 defaultContent: '',
430 className: 'dtr-col-md dtr-break-all'
431 },
432 {
433 title: 'Call',
434 data: 'call',
435 defaultContent: '',
436 className: 'none dtr-col-md dtr-break-all'
437 }
438 ]
439 });
440 }
441 function draw_sasl_logs() {
442 // just recalc width if instance already exists
443 if ($.fn.DataTable.isDataTable('#sasl_logs') ) {
444 $('#sasl_logs').DataTable().columns.adjust().responsive.recalc();
445 return;
446 }
447
448 $('#sasl_logs').DataTable({
449 processing: true,
450 serverSide: false,
451 language: lang_datatables,
452 order: [[0, 'desc']],
453 ajax: {
454 type: "GET",
455 url: "/api/v1/get/logs/sasl",
456 dataSrc: function(data){
457 return process_table_data(data, 'sasl_log_table');
458 }
459 },
460 columns: [
461 {
462 title: lang.username,
463 data: 'username',
464 defaultContent: ''
465 },
466 {
467 title: lang.service,
468 data: 'service',
469 defaultContent: ''
470 },
471 {
472 title: 'IP',
473 data: 'real_rip',
474 defaultContent: '',
475 className: 'dtr-col-md text-break'
476 },
477 {
478 title: lang.login_time,
479 data: 'datetime',
480 defaultContent: '',
481 createdCell: function(td, cellData) {
482 cellData = Math.floor((new Date(data.replace(/-/g, "/"))).getTime() / 1000);
483 createSortableDate(td, cellData)
484 }
485 }
486 ]
487 });
488 }
489 function draw_acme_logs() {
490 // just recalc width if instance already exists
491 if ($.fn.DataTable.isDataTable('#acme_log') ) {
492 $('#acme_log').DataTable().columns.adjust().responsive.recalc();
493 return;
494 }
495
496 $('#acme_log').DataTable({
497 processing: true,
498 serverSide: false,
499 language: lang_datatables,
500 order: [[0, 'desc']],
501 ajax: {
502 type: "GET",
503 url: "/api/v1/get/logs/acme",
504 dataSrc: function(data){
505 return process_table_data(data, 'general_syslog');
506 }
507 },
508 columns: [
509 {
510 title: lang.time,
511 data: 'time',
512 defaultContent: '',
513 createdCell: function(td, cellData) {
514 createSortableDate(td, cellData)
515 }
516 },
517 {
518 title: lang.message,
519 data: 'message',
520 defaultContent: '',
521 className: 'dtr-col-md dtr-break-all'
522 }
523 ]
524 });
525 }
526 function draw_netfilter_logs() {
527 // just recalc width if instance already exists
528 if ($.fn.DataTable.isDataTable('#netfilter_log') ) {
529 $('#netfilter_log').DataTable().columns.adjust().responsive.recalc();
530 return;
531 }
532
533 $('#netfilter_log').DataTable({
534 processing: true,
535 serverSide: false,
536 language: lang_datatables,
537 order: [[0, 'desc']],
538 ajax: {
539 type: "GET",
540 url: "/api/v1/get/logs/netfilter",
541 dataSrc: function(data){
542 return process_table_data(data, 'general_syslog');
543 }
544 },
545 columns: [
546 {
547 title: lang.time,
548 data: 'time',
549 defaultContent: '',
550 createdCell: function(td, cellData) {
551 createSortableDate(td, cellData)
552 }
553 },
554 {
555 title: lang.priority,
556 data: 'priority',
557 defaultContent: ''
558 },
559 {
560 title: lang.message,
561 data: 'message',
562 defaultContent: '',
563 className: 'dtr-col-md text-break'
564 }
565 ]
566 });
567 }
568 function draw_sogo_logs() {
569 // just recalc width if instance already exists
570 if ($.fn.DataTable.isDataTable('#sogo_log') ) {
571 $('#sogo_log').DataTable().columns.adjust().responsive.recalc();
572 return;
573 }
574
575 $('#sogo_log').DataTable({
576 processing: true,
577 serverSide: false,
578 language: lang_datatables,
579 order: [[0, 'desc']],
580 ajax: {
581 type: "GET",
582 url: "/api/v1/get/logs/sogo",
583 dataSrc: function(data){
584 return process_table_data(data, 'general_syslog');
585 }
586 },
587 columns: [
588 {
589 title: lang.time,
590 data: 'time',
591 defaultContent: '',
592 createdCell: function(td, cellData) {
593 createSortableDate(td, cellData)
594 }
595 },
596 {
597 title: lang.priority,
598 data: 'priority',
599 defaultContent: ''
600 },
601 {
602 title: lang.message,
603 data: 'message',
604 defaultContent: '',
605 className: 'dtr-col-md text-break'
606 }
607 ]
608 });
609 }
610 function draw_dovecot_logs() {
611 // just recalc width if instance already exists
612 if ($.fn.DataTable.isDataTable('#dovecot_log') ) {
613 $('#dovecot_log').DataTable().columns.adjust().responsive.recalc();
614 return;
615 }
616
617 $('#dovecot_log').DataTable({
618 processing: true,
619 serverSide: false,
620 language: lang_datatables,
621 order: [[0, 'desc']],
622 ajax: {
623 type: "GET",
624 url: "/api/v1/get/logs/dovecot",
625 dataSrc: function(data){
626 return process_table_data(data, 'general_syslog');
627 }
628 },
629 columns: [
630 {
631 title: lang.time,
632 data: 'time',
633 defaultContent: '',
634 createdCell: function(td, cellData) {
635 createSortableDate(td, cellData)
636 }
637 },
638 {
639 title: lang.priority,
640 data: 'priority',
641 defaultContent: ''
642 },
643 {
644 title: lang.message,
645 data: 'message',
646 defaultContent: '',
647 className: 'dtr-col-md text-break'
648 }
649 ]
650 });
651 }
652 function rspamd_pie_graph() {
653 $.ajax({
654 url: '/api/v1/get/rspamd/actions',
655 async: true,
656 success: function(data){
657 console.log(data);
658
659 var total = 0;
660 $(data).map(function(){total += this[1];});
661 var labels = $.makeArray($(data).map(function(){return this[0] + ' ' + Math.round(this[1]/total * 100) + '%';}));
662 var values = $.makeArray($(data).map(function(){return this[1];}));
663 console.log(values);
664
665 var graphdata = {
666 labels: labels,
667 datasets: [{
668 data: values,
669 backgroundColor: ['#DC3023', '#59ABE3', '#FFA400', '#FFA400', '#26A65B']
670 }]
671 };
672
673 var options = {
674 responsive: true,
675 maintainAspectRatio: false,
676 plugins: {
677 datalabels: {
678 color: '#FFF',
679 font: {
680 weight: 'bold'
681 },
682 display: function(context) {
683 return context.dataset.data[context.dataIndex] !== 0;
684 },
685 formatter: function(value, context) {
686 return Math.round(value/total*100) + '%';
687 }
688 }
689 }
690 };
691 var chartcanvas = document.getElementById('rspamd_donut');
692 Chart.register('ChartDataLabels');
693 if(typeof chart == 'undefined') {
694 chart = new Chart(chartcanvas.getContext("2d"), {
695 plugins: [ChartDataLabels],
696 type: 'doughnut',
697 data: graphdata,
698 options: options
699 });
700 }
701 else {
702 chart.destroy();
703 chart = new Chart(chartcanvas.getContext("2d"), {
704 plugins: [ChartDataLabels],
705 type: 'doughnut',
706 data: graphdata,
707 options: options
708 });
709 }
710 }
711 });
712 }
713 function draw_rspamd_history() {
714 // just recalc width if instance already exists
715 if ($.fn.DataTable.isDataTable('#rspamd_history') ) {
716 $('#rspamd_history').DataTable().columns.adjust().responsive.recalc();
717 return;
718 }
719
720 $('#rspamd_history').DataTable({
721 processing: true,
722 serverSide: false,
723 language: lang_datatables,
724 order: [[0, 'desc']],
725 ajax: {
726 type: "GET",
727 url: "/api/v1/get/logs/rspamd-history",
728 dataSrc: function(data){
729 return process_table_data(data, 'rspamd_history');
730 }
731 },
732 columns: [
733 {
734 title: lang.time,
735 data: 'unix_time',
736 defaultContent: '',
737 createdCell: function(td, cellData) {
738 createSortableDate(td, cellData)
739 }
740 },
741 {
742 title: 'IP address',
743 data: 'ip',
744 defaultContent: ''
745 },
746 {
747 title: 'From',
748 data: 'sender_mime',
749 defaultContent: ''
750 },
751 {
752 title: 'To',
753 data: 'rcpt',
754 defaultContent: ''
755 },
756 {
757 title: 'Subject',
758 data: 'subject',
759 defaultContent: ''
760 },
761 {
762 title: 'Action',
763 data: 'action',
764 defaultContent: ''
765 },
766 {
767 title: 'Score',
768 data: 'score',
769 defaultContent: '',
770 createdCell: function(td, cellData) {
771 $(td).attr({
772 "data-order": cellData.sortBy,
773 "data-sort": cellData.sortBy
774 });
775 $(td).html(cellData.value);
776 }
777 },
778 {
779 title: 'Symbols',
780 data: 'symbols',
781 defaultContent: '',
782 className: 'none dtr-col-md'
783 },
784 {
785 title: 'Msg size',
786 data: 'size',
787 defaultContent: ''
788 },
789 {
790 title: 'Scan Time',
791 data: 'scan_time',
792 defaultContent: '',
793 createdCell: function(td, cellData) {
794 $(td).attr({
795 "data-order": cellData.sortBy,
796 "data-sort": cellData.sortBy
797 });
798 $(td).html(cellData.value);
799 }
800 },
801 {
802 title: 'ID',
803 data: 'message-id',
804 defaultContent: ''
805 },
806 {
807 title: 'Authenticated user',
808 data: 'user',
809 defaultContent: ''
810 }
811 ]
812 });
813 }
814 function process_table_data(data, table) {
815 if (table == 'rspamd_history') {
816 $.each(data, function (i, item) {
817 if (item.rcpt_mime != "") {
818 item.rcpt = escapeHtml(item.rcpt_mime.join(", "));
819 }
820 else {
821 item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
822 }
823 item.symbols = Object.keys(item.symbols).sort(function (a, b) {
824 if (item.symbols[a].score === 0) return 1
825 if (item.symbols[b].score === 0) return -1
826 if (item.symbols[b].score < 0 && item.symbols[a].score < 0) {
827 return item.symbols[a].score - item.symbols[b].score
828 }
829 if (item.symbols[b].score > 0 && item.symbols[a].score > 0) {
830 return item.symbols[b].score - item.symbols[a].score
831 }
832 return item.symbols[b].score - item.symbols[a].score
833 }).map(function(key) {
834 var sym = item.symbols[key];
835 if (sym.score < 0) {
836 sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
837 }
838 else if (sym.score === 0) {
839 sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
840 }
841 else {
842 sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
843 }
844 var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
845 if (sym.options) {
846 str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
847 }
848 return str
849 }).join('<br>\n');
850 item.subject = escapeHtml(item.subject);
851 var scan_time = item.time_real.toFixed(3);
852 if (item.time_virtual) {
853 scan_time += ' / ' + item.time_virtual.toFixed(3);
854 }
855 item.scan_time = {
856 "sortBy": item.time_real,
857 "value": scan_time
858 };
859 if (item.action === 'clean' || item.action === 'no action') {
860 item.action = "<div class='badge fs-6 bg-success'>" + item.action + "</div>";
861 } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
862 item.action = "<div class='badge fs-6 bg-warning'>" + item.action + "</div>";
863 } else if (item.action === 'spam' || item.action === 'reject') {
864 item.action = "<div class='badge fs-6 bg-danger'>" + item.action + "</div>";
865 } else {
866 item.action = "<div class='badge fs-6 bg-info'>" + item.action + "</div>";
867 }
868 var score_content;
869 if (item.score < item.required_score) {
870 score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
871 } else {
872 score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
873 }
874 item.score = {
875 "sortBy": item.score,
876 "value": score_content
877 };
878 if (item.user == null) {
879 item.user = "none";
880 }
881 });
882 } else if (table == 'autodiscover_log') {
883 $.each(data, function (i, item) {
884 if (item.ua == null) {
885 item.ua = 'unknown';
886 } else {
887 item.ua = escapeHtml(item.ua);
888 }
889 item.ua = '<span style="font-size:small">' + item.ua + '</span>';
890 if (item.service == "activesync") {
891 item.service = '<span class="badge fs-6 bg-info">ActiveSync</span>';
892 }
893 else if (item.service == "imap") {
894 item.service = '<span class="badge fs-6 bg-success">IMAP, SMTP, Cal-/CardDAV</span>';
895 }
896 else {
897 item.service = '<span class="badge fs-6 bg-danger">' + escapeHtml(item.service) + '</span>';
898 }
899 });
900 } else if (table == 'watchdog') {
901 $.each(data, function (i, item) {
902 if (item.message == null) {
903 item.message = 'Health level: ' + item.lvl + '% (' + item.hpnow + '/' + item.hptotal + ')';
904 if (item.hpdiff < 0) {
905 item.trend = '<span class="badge fs-6 bg-danger"><i class="bi bi-caret-down-fill"></i> ' + item.hpdiff + '</span>';
906 }
907 else if (item.hpdiff == 0) {
908 item.trend = '<span class="badge fs-6 bg-info"><i class="bi bi-caret-right-fill"></i> ' + item.hpdiff + '</span>';
909 }
910 else {
911 item.trend = '<span class="badge fs-6 bg-success"><i class="bi bi-caret-up-fill"></i> ' + item.hpdiff + '</span>';
912 }
913 }
914 else {
915 item.trend = '';
916 item.service = '';
917 }
918 });
919 } else if (table == 'mailcow_ui') {
920 $.each(data, function (i, item) {
921 if (item === null) { return true; }
922 item.user = escapeHtml(item.user);
923 item.call = escapeHtml(item.call);
924 item.task = '<code>' + item.task + '</code>';
925 item.type = '<span class="badge fs-6 bg-' + item.type + '">' + item.type + '</span>';
926 });
927 } else if (table == 'sasl_log_table') {
928 $.each(data, function (i, item) {
929 if (item === null) { return true; }
930 item.username = escapeHtml(item.username);
931 item.service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
932 });
933 } else if (table == 'general_syslog') {
934 $.each(data, function (i, item) {
935 if (item === null) { return true; }
936 if (item.message.match("^base64,")) {
937 try {
938 item.message = atob(item.message.slice(7)).replace(/\\n/g, "<br />");
939 } catch(e) {
940 item.message = item.message.slice(7);
941 }
942 } else {
943 item.message = escapeHtml(item.message);
944 }
945 item.call = escapeHtml(item.call);
946 var danger_class = ["emerg", "alert", "crit", "err"];
947 var warning_class = ["warning", "warn"];
948 var info_class = ["notice", "info", "debug"];
949 if (jQuery.inArray(item.priority, danger_class) !== -1) {
950 item.priority = '<span class="badge fs-6 bg-danger">' + item.priority + '</span>';
951 } else if (jQuery.inArray(item.priority, warning_class) !== -1) {
952 item.priority = '<span class="badge fs-6 bg-warning">' + item.priority + '</span>';
953 } else if (jQuery.inArray(item.priority, info_class) !== -1) {
954 item.priority = '<span class="badge fs-6 bg-info">' + item.priority + '</span>';
955 }
956 });
957 } else if (table == 'apilog') {
958 $.each(data, function (i, item) {
959 if (item === null) { return true; }
960 if (item.method == 'GET') {
961 item.method = '<span class="badge fs-6 bg-success">' + item.method + '</span>';
962 } else if (item.method == 'POST') {
963 item.method = '<span class="badge fs-6 bg-warning">' + item.method + '</span>';
964 }
965 item.data = escapeHtml(item.data);
966 });
967 } else if (table == 'rllog') {
968 $.each(data, function (i, item) {
969 if (item.user == null) {
970 item.user = "none";
971 }
972 if (item.rl_hash == null) {
973 item.rl_hash = "err";
974 }
975 item.indicator = '<span style="border-right:6px solid #' + intToRGB(hashCode(item.rl_hash)) + ';padding-left:5px;">&nbsp;</span>';
976 if (item.rl_hash != 'err') {
977 item.action = '<a href="#" data-action="delete_selected" data-id="single-hash" data-api-url="delete/rlhash" data-item="' + encodeURI(item.rl_hash) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.reset_limit + '</a>';
978 }
979 });
980 }
981 return data
982 };
983 $('.add_log_lines').on('click', function (e) {
984 e.preventDefault();
985 var log_table= $(this).data("table")
986 var new_nrows = $(this).data("nrows")
987 var post_process = $(this).data("post-process")
988 var log_url = $(this).data("log-url")
989 if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
990 console.log("no data-table or data-nrows or log_url or data-post-process attr found");
991 return;
992 }
993
994 if (table = $('#' + log_table).DataTable()) {
995 var heading = $('#' + log_table).closest('.card').find('.card-header');
996 var load_rows = (table.page.len() + 1) + '-' + (table.page.len() + new_nrows)
997
998 $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
999 if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
1000 var rows = process_table_data(data, post_process);
1001 var rows_now = (table.page.len() + data.length);
1002 $(heading).children('.table-lines').text(rows_now)
1003 mailcow_alert_box(data.length + lang.additional_rows, "success");
1004 table.rows.add(rows).draw();
1005 });
1006 }
1007 })
1008
1009 // detect element visibility changes
1010 function onVisible(element, callback) {
1011 $(document).ready(function() {
1012 element_object = document.querySelector(element);
1013 if (element_object === null) return;
1014
1015 new IntersectionObserver((entries, observer) => {
1016 entries.forEach(entry => {
1017 if(entry.intersectionRatio > 0) {
1018 callback(element_object);
1019 }
1020 });
1021 }).observe(element_object);
1022 });
1023 }
1024 // Draw Table if tab is active
1025 onVisible("[id^=postfix_log]", () => draw_postfix_logs());
1026 onVisible("[id^=dovecot_log]", () => draw_dovecot_logs());
1027 onVisible("[id^=sogo_log]", () => draw_sogo_logs());
1028 onVisible("[id^=watchdog_log]", () => draw_watchdog_logs());
1029 onVisible("[id^=autodiscover_log]", () => draw_autodiscover_logs());
1030 onVisible("[id^=acme_log]", () => draw_acme_logs());
1031 onVisible("[id^=api_log]", () => draw_api_logs());
1032 onVisible("[id^=rl_log]", () => draw_rl_logs());
1033 onVisible("[id^=ui_logs]", () => draw_ui_logs());
1034 onVisible("[id^=sasl_logs]", () => draw_sasl_logs());
1035 onVisible("[id^=netfilter_log]", () => draw_netfilter_logs());
1036 onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
1037 onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
1038
1039
1040
1041 // start polling host stats if tab is active
1042 onVisible("[id^=tab-containers]", () => update_stats());
1043 // start polling container stats if collapse is active
1044 var containerElements = document.querySelectorAll(".container-details-collapse");
1045 for (let i = 0; i < containerElements.length; i++){
1046 new IntersectionObserver((entries, observer) => {
1047 entries.forEach(entry => {
1048 if(entry.intersectionRatio > 0) {
1049
1050 if (!containerElements[i].classList.contains("show")){
1051 var container = containerElements[i].id.replace("Collapse", "");
1052 var container_id = containerElements[i].getAttribute("data-id");
1053
1054 // check if chart exists or needs to be created
1055 if (!Chart.getChart(container + "_DiskIOChart"))
1056 createReadWriteChart(container + "_DiskIOChart", "Read", "Write");
1057 if (!Chart.getChart(container + "_NetIOChart"))
1058 createReadWriteChart(container + "_NetIOChart", "Recv", "Sent");
1059
1060 // add container to polling list
1061 containersToUpdate[container] = {
1062 id: container_id,
1063 state: "idle"
1064 }
1065
1066 // stop polling if collapse is closed
1067 containerElements[i].addEventListener('hidden.bs.collapse', function () {
1068 var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
1069 var netIOCtx = Chart.getChart(container + "_NetIOChart");
1070
1071 diskIOCtx.data.datasets[0].data = [];
1072 diskIOCtx.data.datasets[1].data = [];
1073 diskIOCtx.data.labels = [];
1074 netIOCtx.data.datasets[0].data = [];
1075 netIOCtx.data.datasets[1].data = [];
1076 netIOCtx.data.labels = [];
1077
1078 diskIOCtx.update();
1079 netIOCtx.update();
1080
1081 delete containersToUpdate[container];
1082 });
1083 }
1084
1085 }
1086 });
1087 }).observe(containerElements[i]);
1088 }
1089});
1090
1091
1092// update system stats - every 5 seconds if system & container tab is active
1093function update_stats(timeout=5){
1094 if (!$('#tab-containers').hasClass('active')) {
1095 // tab not active - dont fetch stats - run again in n seconds
1096 return;
1097 }
1098
1099 window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) {
1100 return response.json();
1101 }).then(function(data) {
1102 console.log(data);
1103
1104 if (data){
1105 // display table data
1106 $("#host_date").text(data.system_time);
1107 $("#host_uptime").text(formatUptime(data.uptime));
1108 $("#host_cpu_cores").text(data.cpu.cores);
1109 $("#host_cpu_usage").text(parseInt(data.cpu.usage).toString() + "%");
1110 $("#host_memory_total").text((data.memory.total / (1024 ** 3)).toFixed(2).toString() + "GB");
1111 $("#host_memory_usage").text(parseInt(data.memory.usage).toString() + "%");
1112
1113 // update cpu and mem chart
1114 var cpu_chart = Chart.getChart("host_cpu_chart");
1115 var mem_chart = Chart.getChart("host_mem_chart");
1116
1117 cpu_chart.data.labels.push(data.system_time.split(" ")[1]);
1118 if (cpu_chart.data.labels.length > 30) cpu_chart.data.labels.shift();
1119 mem_chart.data.labels.push(data.system_time.split(" ")[1]);
1120 if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift();
1121
1122 cpu_chart.data.datasets[0].data.push(data.cpu.usage);
1123 if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift();
1124 mem_chart.data.datasets[0].data.push(data.memory.usage);
1125 if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift();
1126
1127 cpu_chart.update();
1128 mem_chart.update();
1129 }
1130
1131 // run again in n seconds
1132 setTimeout(update_stats, timeout * 1000);
1133 });
1134}
1135// update specific container stats - every n (default 5s) seconds
1136function update_container_stats(timeout=5){
1137
1138 if ($('#tab-containers').hasClass('active')) {
1139 for (let container in containersToUpdate){
1140 container_id = containersToUpdate[container].id;
1141 // check if container update stats is already running
1142 if (containersToUpdate[container].state == "running")
1143 continue;
1144 containersToUpdate[container].state = "running";
1145
1146
1147 window.fetch("/api/v1/get/status/container/" + container_id, {method:'GET',cache:'no-cache'}).then(function(response) {
1148 return response.json();
1149 }).then(function(data) {
1150 var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
1151 var netIOCtx = Chart.getChart(container + "_NetIOChart");
1152
1153 console.log(container);
1154 console.log(data);
1155 prev_stats = null;
1156 if (data.length >= 2){
1157 prev_stats = data[data.length -2];
1158
1159 // hide spinners if we collected enough data
1160 $('#' + container + "_DiskIOChart").removeClass('d-none');
1161 $('#' + container + "_DiskIOChart").prev().addClass('d-none');
1162 $('#' + container + "_NetIOChart").removeClass('d-none');
1163 $('#' + container + "_NetIOChart").prev().addClass('d-none');
1164 }
1165
1166 data = data[data.length -1];
1167
1168 if (prev_stats != null){
1169 // calc time diff
1170 var time_diff = (new Date(data.read) - new Date(prev_stats.read)) / 1000;
1171
1172 // calc disk io b/s
1173 if ('io_service_bytes_recursive' in prev_stats.blkio_stats && prev_stats.blkio_stats.io_service_bytes_recursive !== null){
1174 var prev_read_bytes = 0;
1175 var prev_write_bytes = 0;
1176 for (var i = 0; i < prev_stats.blkio_stats.io_service_bytes_recursive.length; i++){
1177 if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "read")
1178 prev_read_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
1179 else if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "write")
1180 prev_write_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
1181 }
1182 var read_bytes = 0;
1183 var write_bytes = 0;
1184 for (var i = 0; i < data.blkio_stats.io_service_bytes_recursive.length; i++){
1185 if (data.blkio_stats.io_service_bytes_recursive[i].op == "read")
1186 read_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
1187 else if (data.blkio_stats.io_service_bytes_recursive[i].op == "write")
1188 write_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
1189 }
1190 var diff_bytes_read = (read_bytes - prev_read_bytes) / time_diff;
1191 var diff_bytes_write = (write_bytes - prev_write_bytes) / time_diff;
1192 }
1193
1194 // calc net io b/s
1195 if ('networks' in prev_stats){
1196 var prev_recv_bytes = 0;
1197 var prev_sent_bytes = 0;
1198 for (var key in prev_stats.networks){
1199 prev_recv_bytes += prev_stats.networks[key].rx_bytes;
1200 prev_sent_bytes += prev_stats.networks[key].tx_bytes;
1201 }
1202 var recv_bytes = 0;
1203 var sent_bytes = 0;
1204 for (var key in data.networks){
1205 recv_bytes += data.networks[key].rx_bytes;
1206 sent_bytes += data.networks[key].tx_bytes;
1207 }
1208 var diff_bytes_recv = (recv_bytes - prev_recv_bytes) / time_diff;
1209 var diff_bytes_sent = (sent_bytes - prev_sent_bytes) / time_diff;
1210 }
1211
1212 addReadWriteChart(diskIOCtx, diff_bytes_read, diff_bytes_write, "");
1213 addReadWriteChart(netIOCtx, diff_bytes_recv, diff_bytes_sent, "");
1214 }
1215
1216 // run again in n seconds
1217 containersToUpdate[container].state = "idle";
1218 }).catch(err => {
1219 console.log(err);
1220 });
1221 }
1222 }
1223
1224 // run again in n seconds
1225 setTimeout(update_container_stats, timeout * 1000);
1226}
1227// get public ips
1228function get_public_ips(){
1229 window.fetch("/api/v1/get/status/host/ip", {method:'GET',cache:'no-cache'}).then(function(response) {
1230 return response.json();
1231 }).then(function(data) {
1232 console.log(data);
1233
1234 // display host ips
1235 if (data.ipv4)
1236 $("#host_ipv4").text(data.ipv4);
1237 if (data.ipv6)
1238 $("#host_ipv6").text(data.ipv6);
1239 });
1240}
1241// format hosts uptime seconds to readable string
1242function formatUptime(seconds){
1243 seconds = Number(seconds);
1244 var d = Math.floor(seconds / (3600*24));
1245 var h = Math.floor(seconds % (3600*24) / 3600);
1246 var m = Math.floor(seconds % 3600 / 60);
1247 var s = Math.floor(seconds % 60);
1248
1249 var dFormat = d > 0 ? d + "D " : "";
1250 var hFormat = h > 0 ? h + "H " : "";
1251 var mFormat = m > 0 ? m + "M " : "";
1252 var sFormat = s > 0 ? s + "S" : "";
1253 return dFormat + hFormat + mFormat + sFormat;
1254}
1255// format bytes to readable string
1256function formatBytes(bytes){
1257 // b
1258 if (bytes < 1000) return bytes.toFixed(2).toString()+' B/s';
1259 // b to kb
1260 bytes = bytes / 1024;
1261 if (bytes < 1000) return bytes.toFixed(2).toString()+' KB/s';
1262 // kb to mb
1263 bytes = bytes / 1024;
1264 if (bytes < 1000) return bytes.toFixed(2).toString()+' MB/s';
1265 // final mb to gb
1266 return (bytes / 1024).toFixed(2).toString()+' GB/s';
1267}
1268// create read write line chart
1269function createReadWriteChart(chart_id, read_lable, write_lable){
1270 var ctx = document.getElementById(chart_id);
1271
1272 var dataNet = {
1273 labels: [],
1274 datasets: [{
1275 label: read_lable,
1276 backgroundColor: "rgba(41, 187, 239, 0.3)",
1277 borderColor: "rgba(41, 187, 239, 0.6)",
1278 pointRadius: 1,
1279 pointHitRadius: 6,
1280 borderWidth: 2,
1281 fill: true,
1282 tension: 0.2,
1283 data: []
1284 }, {
1285 label: write_lable,
1286 backgroundColor: "rgba(239, 60, 41, 0.3)",
1287 borderColor: "rgba(239, 60, 41, 0.6)",
1288 pointRadius: 1,
1289 pointHitRadius: 6,
1290 borderWidth: 2,
1291 fill: true,
1292 tension: 0.2,
1293 data: []
1294 }]
1295 };
1296 var optionsNet = {
1297 interaction: {
1298 mode: 'index'
1299 },
1300 scales: {
1301 yAxis: {
1302 min: 0,
1303 grid: {
1304 display: false
1305 },
1306 ticks: {
1307 callback: function(i, index, ticks) {
1308 return formatBytes(i);
1309 }
1310 }
1311 },
1312 xAxis: {
1313 grid: {
1314 display: false
1315 }
1316 }
1317 }
1318 };
1319
1320 return new Chart(ctx, {
1321 type: 'line',
1322 data: dataNet,
1323 options: optionsNet
1324 });
1325}
1326// add to read write line chart
1327function addReadWriteChart(chart_context, read_point, write_point, time, limit = 30){
1328 // push time label for x-axis
1329 chart_context.data.labels.push(time);
1330 if (chart_context.data.labels.length > limit) chart_context.data.labels.shift();
1331
1332 // push datapoints
1333 chart_context.data.datasets[0].data.push(read_point);
1334 chart_context.data.datasets[1].data.push(write_point);
1335 // shift data if more than 20 entires exists
1336 if (chart_context.data.datasets[0].data.length > limit) chart_context.data.datasets[0].data.shift();
1337 if (chart_context.data.datasets[1].data.length > limit) chart_context.data.datasets[1].data.shift();
1338
1339 chart_context.update();
1340}
1341// create host cpu and mem chart
1342function createHostCpuAndMemChart(){
1343 var cpu_ctx = document.getElementById("host_cpu_chart");
1344 var mem_ctx = document.getElementById("host_mem_chart");
1345
1346 var dataCpu = {
1347 labels: [],
1348 datasets: [{
1349 label: "CPU %",
1350 backgroundColor: "rgba(41, 187, 239, 0.3)",
1351 borderColor: "rgba(41, 187, 239, 0.6)",
1352 pointRadius: 1,
1353 pointHitRadius: 6,
1354 borderWidth: 2,
1355 fill: true,
1356 tension: 0.2,
1357 data: []
1358 }]
1359 };
1360 var optionsCpu = {
1361 interaction: {
1362 mode: 'index'
1363 },
1364 scales: {
1365 yAxis: {
1366 min: 0,
1367 grid: {
1368 display: false
1369 },
1370 ticks: {
1371 callback: function(i, index, ticks) {
1372 return i.toFixed(0).toString() + "%";
1373 }
1374 }
1375 },
1376 xAxis: {
1377 grid: {
1378 display: false
1379 }
1380 }
1381 }
1382 };
1383
1384 var dataMem = {
1385 labels: [],
1386 datasets: [{
1387 label: "MEM %",
1388 backgroundColor: "rgba(41, 187, 239, 0.3)",
1389 borderColor: "rgba(41, 187, 239, 0.6)",
1390 pointRadius: 1,
1391 pointHitRadius: 6,
1392 borderWidth: 2,
1393 fill: true,
1394 tension: 0.2,
1395 data: []
1396 }]
1397 };
1398 var optionsMem = {
1399 interaction: {
1400 mode: 'index'
1401 },
1402 scales: {
1403 yAxis: {
1404 min: 0,
1405 grid: {
1406 display: false
1407 },
1408 ticks: {
1409 callback: function(i, index, ticks) {
1410 return i.toFixed(0).toString() + "%";
1411 }
1412 }
1413 },
1414 xAxis: {
1415 grid: {
1416 display: false
1417 }
1418 }
1419 }
1420 };
1421
1422
1423 var net_io_chart = new Chart(cpu_ctx, {
1424 type: 'line',
1425 data: dataCpu,
1426 options: optionsCpu
1427 });
1428 var disk_io_chart = new Chart(mem_ctx, {
1429 type: 'line',
1430 data: dataMem,
1431 options: optionsMem
1432 });
1433}
1434// check for mailcow updates
1435function check_update(current_version, github_repo_url){
1436 if (!current_version || !github_repo_url) return false;
1437
1438 var github_account = github_repo_url.split("/")[3];
1439 var github_repo_name = github_repo_url.split("/")[4];
1440
1441 // get details about latest release
1442 window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/latest", {method:'GET',cache:'no-cache'}).then(function(response) {
1443 return response.json();
1444 }).then(function(latest_data) {
1445 // get details about current release
1446 window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/tags/"+current_version, {method:'GET',cache:'no-cache'}).then(function(response) {
1447 return response.json();
1448 }).then(function(current_data) {
1449 // compare releases
1450 var date_current = new Date(current_data.created_at);
1451 var date_latest = new Date(latest_data.created_at);
1452 if (date_latest.getTime() <= date_current.getTime()){
1453 // no update available
1454 $("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success");
1455 $("#mailcow_update").html("<b>" + lang_debug.no_update_available + "</b>");
1456 } else {
1457 // update available
1458 $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
1459 $("#mailcow_update").html(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+latest_data.tag_name+`</a>`);
1460 $("#mailcow_update_changelog").click(function(){
1461 if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
1462 return;
1463
1464 showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name);
1465 })
1466 }
1467 }).catch(err => {
1468 // err
1469 console.log(err);
1470 $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
1471 $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
1472 });
1473 }).catch(err => {
1474 // err
1475 console.log(err);
1476 $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
1477 $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
1478 });
1479}
1480// show version changelog modal
1481function showVersionModal(title, version){
1482 $.ajax({
1483 type: 'GET',
1484 url: 'https://api.github.com/repos/' + mailcow_info.project_owner + '/' + mailcow_info.project_repo + '/releases/tags/' + version,
1485 dataType: 'json',
1486 success: function (data) {
1487 var md = window.markdownit();
1488 var result = md.render(data.body);
1489 result = parseGithubMarkdownLinks(result);
1490
1491 $('#showVersionModal').find(".modal-title").html(title);
1492 $('#showVersionModal').find(".modal-body").html(`
1493 <h3>` + data.name + `</h3>
1494 <span class="mt-4">` + result + `</span>
1495 <span><b>Github Link:</b>
1496 <a target="_blank" href="https://github.com/` + mailcow_info.project_owner + `/` + mailcow_info.project_repo + `/releases/tag/` + version + `">` + version + `</a>
1497 </span>
1498 `);
1499
1500 new bootstrap.Modal(document.getElementById("showVersionModal"), {
1501 backdrop: 'static',
1502 keyboard: false
1503 }).show();
1504 }
1505 });
1506}
1507function parseGithubMarkdownLinks(inputText) {
1508 var replacedText, replacePattern1;
1509
1510 replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
1511 replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
1512 if (matched.includes('github.com')){
1513 // return short link if it's github link
1514 last_uri_path = matched.split('/');
1515 last_uri_path = last_uri_path[last_uri_path.length - 1];
1516
1517 // adjust Full Changelog link to match last git version and new git version, if link is a compare link
1518 if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
1519 matched = matched.replace(last_uri_path, mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
1520 last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
1521 }
1522
1523 return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
1524 };
1525
1526 // if it's not a github link, return complete link
1527 return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
1528 });
1529
1530 return replacedText;
1531}
1532
1533function convertTimestampToLocalFormat(timestamp) {
1534 var date = new Date(timestamp ? timestamp * 1000 : 0);
1535 return date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
1536}