const LOCALE = undefined; const DATETIME_FORMAT = { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }; $(document).ready(function() { // mailcow alert box generator window.mailcow_alert_box = function(message, type) { msg = $('').text(message).text(); if (type == 'danger' || type == 'info') { auto_hide = 0; $('#' + localStorage.getItem("add_modal")).modal('show'); localStorage.removeItem("add_modal"); } else { auto_hide = 5000; } $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}}); } $(".generate_password").click(async function( event ) { try { var password_policy = await window.fetch("/api/v1/get/passwordpolicy", { method:'GET', cache:'no-cache' }); var password_policy = await password_policy.json(); random_passwd_length = password_policy.length; } catch(err) { var random_passwd_length = 8; } event.preventDefault(); $('[data-hibp]').trigger('input'); if (typeof($(this).closest("form").data('pwgen-length')) == "number") { var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length')) } else { var random_passwd = GPW.pronounceable(random_passwd_length) } $(this).closest("form").find('[data-pwgen-field]').attr('type', 'text'); $(this).closest("form").find('[data-pwgen-field]').val(random_passwd); }); function str_rot13(str) { return (str + '').replace(/[a-z]/gi, function(s){ return String.fromCharCode(s.charCodeAt(0) + (s.toLowerCase() < 'n' ? 13 : -13)) }) } $(".rot-enc").html(function(){ footer_html = $(this).html(); footer_html = footer_html.replace(/</g, '<').replace(/>/g, '>') .replace(/&/g, '&').replace(/&nzc;/g, '&') .replace(/"/g, '"').replace(/'/g, "'"); return str_rot13(footer_html) }); // https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate function shake(div,interval,distance,times) { if(typeof interval === 'undefined') { interval = 100; } if(typeof distance === 'undefined') { distance = 10; } if(typeof times === 'undefined') { times = 4; } $(div).css('position','relative'); for(var iter=0;iter<(times+1);iter++){ $(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval); } $(div).animate({ left: 0},interval); } // form cache $('[data-cached-form="true"]').formcache({key: $(this).data('id')}); // tooltips $(function () { $('[data-bs-toggle="tooltip"]').tooltip() }); // remember last navigation pill (function () { 'use strict'; // remember desktop tabs $('button[data-bs-toggle="tab"]').on('click', function (e) { if ($(this).data('dont-remember') == 1) { return true; } var id = $(this).parents('[role="tablist"]').attr('id'); var key = 'lastTag'; if (id) { key += ':' + id; } var tab_id = $(e.target).attr('data-bs-target').substring(1); localStorage.setItem(key, tab_id); }); // remember mobile tabs $('button[data-bs-target^="#collapse-tab-"]').on('click', function (e) { // only remember tab if its being opened if ($(this).hasClass('collapsed')) return false; var tab_id = $(this).closest('div[role="tabpanel"]').attr('id'); if ($(this).data('dont-remember') == 1) { return true; } var id = $(this).parents('[role="tablist"]').attr('id');; var key = 'lastTag'; if (id) { key += ':' + id; } localStorage.setItem(key, tab_id); }); // open last tab $('[role="tablist"]').each(function (idx, elem) { var id = $(elem).attr('id'); var key = 'lastTag'; if (id) { key += ':' + id; } var lastTab = localStorage.getItem(key); if (lastTab) { $('[data-bs-target="#' + lastTab + '"]').click(); var tab = $('[id^="' + lastTab + '"]'); $(tab).find('.card-body.collapse:first').collapse('show'); } }); })(); // responsive tabs, scroll to opened tab $(document).on("shown.bs.collapse shown.bs.tab", function (e) { var target = $(e.target); if($(window).width() <= 767) { var offset = target.offset().top - 60; $("html, body").stop().animate({ scrollTop: offset }, 100); } }); // IE fix to hide scrollbars when table body is empty $('tbody').filter(function (index) { return $(this).children().length < 1; }).remove(); // selectpicker $('select').selectpicker({ 'styleBase': 'btn btn-xs-lg', 'noneSelectedText': lang_footer.nothing_selected }); // haveibeenpwned and passwd policy $.ajax({ url: '/api/v1/get/passwordpolicy/html', type: 'GET', success: function(res) { $(".hibp-out").after(res); } }); $('[data-hibp]').after('

' + lang_footer.hibp_check + '

'); $('[data-hibp]').on('input', function() { out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out'); }); $('.haveibeenpwned:not(.task-running)').on('click', function() { var hibp_field = $(this) $(hibp_field).addClass('task-running'); var hibp_result = $(hibp_field).next('.hibp-out') var password_field = $(this).prev('[data-hibp]') if ($(password_field).val() == '') { shake(password_field); } else { $(hibp_result).attr('class', 'hibp-out badge fs-5 bg-info'); $(hibp_result).text(lang_footer.loading); var password_digest = $.sha1($(password_field).val()) var digest_five = password_digest.substring(0, 5).toUpperCase(); var queryURL = "https://api.pwnedpasswords.com/range/" + digest_five; var compl_digest = password_digest.substring(5, 41).toUpperCase(); $.ajax({ url: queryURL, type: 'GET', success: function(res) { if (res.search(compl_digest) > -1){ $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-danger'); $(hibp_result).text(lang_footer.hibp_nok) } else { $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-success'); $(hibp_result).text(lang_footer.hibp_ok) } $(hibp_field).removeClass('task-running'); }, error: function(xhr, status, error) { $(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-warning'); $(hibp_result).text('API error: ' + xhr.responseText) $(hibp_field).removeClass('task-running'); } }); } }); // Disable disallowed inputs $('[data-acl="0"]').each(function(event){ if ($(this).is("a")) { $(this).removeAttr("data-bs-toggle"); $(this).removeAttr("data-bs-target"); $(this).removeAttr("data-action"); $(this).click(function(event) { event.preventDefault(); }); } if ($(this).is("select")) { $(this).selectpicker('destroy'); $(this).replaceWith(function() { return ''; }); } if ($(this).hasClass('btn-group')) { $(this).find('a').each(function(){ $(this).removeClass('dropdown-toggle') .removeAttr('data-bs-toggle') .removeAttr('data-bs-target') .removeAttr('data-action') .removeAttr('id') .attr("disabled", true); $(this).click(function(event) { event.preventDefault(); return; }); }); $(this).find('button').each(function() { $(this).attr("disabled", true); }); } else if ($(this).hasClass('input-group')) { $(this).find('input').each(function() { $(this).removeClass('dropdown-toggle') .removeAttr('data-bs-toggle') .attr("disabled", true); $(this).click(function(event) { event.preventDefault(); }); }); $(this).find('button').each(function() { $(this).attr("disabled", true); }); } else if ($(this).hasClass('form-group')) { $(this).find('input').each(function() { $(this).attr("disabled", true); }); } else if ($(this).hasClass('btn')) { $(this).attr("disabled", true); } else if ($(this).attr('data-provide') == 'slider') { $(this).attr('disabled', true); } else if ($(this).is(':checkbox')) { $(this).attr("disabled", true); } $(this).data("toggle", "tooltip"); $(this).attr("title", lang_acl.prohibited); $(this).tooltip(); }); // disable submit after submitting form (not API driven buttons) $('form').submit(function() { if ($('form button[type="submit"]').data('submitted') == '1') { return false; } else { $(this).find('button[type="submit"]').first().text(lang_footer.loading); $('form button[type="submit"]').attr('data-submitted', '1'); function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); }; $(document).on("keydown", disableF5); } }); // Textarea line numbers $(".textarea-code").numberedtextarea({allowTabChar: true}); // trigger container restart $('#RestartContainer').on('show.bs.modal', function(e) { var container = $(e.relatedTarget).data('container'); $('#containerName').text(container); $('#triggerRestartContainer').click(function(){ $(this).prop("disabled",true); $(this).html('
Loading...
'); $('#statusTriggerRestartContainer').html(lang_footer.restarting_container); $.ajax({ method: 'get', url: '/inc/ajax/container_ctrl.php', timeout: docker_timeout, data: { 'service': container, 'action': 'restart' } }) .always( function (data, status) { $('#statusTriggerRestartContainer').append(data); var htmlResponse = $.parseHTML(data) if ($(htmlResponse).find('span').hasClass('text-success')) { $('#triggerRestartContainer').html(' '); setTimeout(function(){ $('#RestartContainer').modal('toggle'); window.location = window.location.href.split("#")[0]; }, 1200); } else { $('#triggerRestartContainer').html(' '); } }) }); }) // Jquery Datatables, enable responsive plugin $.extend($.fn.dataTable.defaults, { responsive: true }); // disable default datatable click listener $(document).off('click', 'tbody>tr'); // tag boxes $('.tag-box .tag-add').click(function(){ addTag(this); }); $(".tag-box .tag-input").keydown(function (e) { if (e.which == 13){ e.preventDefault(); addTag(this); } }); // Dark Mode Loader $('#dark-mode-toggle').click(toggleDarkMode); if ($('#dark-mode-theme').length) { $('#dark-mode-toggle').prop('checked', true); $('.main-logo').addClass('d-none'); $('.main-logo-dark').removeClass('d-none'); if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png'); if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png'); } else { $('.main-logo').removeClass('d-none'); $('.main-logo-dark').addClass('d-none'); } function toggleDarkMode(){ if($('#dark-mode-theme').length){ $('#dark-mode-theme').remove(); $('#dark-mode-toggle').prop('checked', false); $('.main-logo').removeClass('d-none'); $('.main-logo-dark').addClass('d-none'); if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png'); if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png'); localStorage.setItem('mailcow_theme', 'light'); }else{ $('head').append(''); $('#dark-mode-toggle').prop('checked', true); $('.main-logo').addClass('d-none'); $('.main-logo-dark').removeClass('d-none'); if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png'); if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png'); localStorage.setItem('mailcow_theme', 'dark'); } } // Reveal Password Input $(".reveal-password-input").on('click', '.toggle-password', function() { $(this).parent().find('.toggle-password').children().toggleClass("bi-eye bi-eye-slash"); var input = $(this).parent().find('.password-field') if (input.attr("type") === "password") { input.attr("type", "text"); } else { input.attr("type", "password"); } }); }); // https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery function escapeHtml(n){var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})} function unescapeHtml(t){var n={"&":"&","<":"<",">":">",""":'"',"'":"'","/":"/","`":"`","=":"="};return String(t).replace(/&|<|>|"|'|/|`|=/g,function(t){return n[t]})} function addTag(tagAddElem, tag = null){ var tagboxElem = $(tagAddElem).parent(); var tagInputElem = $(tagboxElem).find(".tag-input")[0]; var tagValuesElem = $(tagboxElem).find(".tag-values")[0]; if (!tag) tag = $(tagInputElem).val(); if (!tag) return; var value_tags = []; try { value_tags = JSON.parse($(tagValuesElem).val()); } catch {} if (!Array.isArray(value_tags)) value_tags = []; if (value_tags.includes(tag)) return; $(' ' + escapeHtml(tag) + '').insertBefore('.tag-input').click(function(){ var del_tag = unescapeHtml($(this).text()); var del_tags = []; try { del_tags = JSON.parse($(tagValuesElem).val()); } catch {} if (Array.isArray(del_tags)){ del_tags.splice(del_tags.indexOf(del_tag), 1); $(tagValuesElem).val(JSON.stringify(del_tags)); } $(this).remove(); }); value_tags.push(tag); $(tagValuesElem).val(JSON.stringify(value_tags)); $(tagInputElem).val(''); } function copyToClipboard(id) { var copyText = document.getElementById(id); copyText.select(); copyText.setSelectionRange(0, 99999); // only works with https connections navigator.clipboard.writeText(copyText.value); mailcow_alert_box(lang.copy_to_clipboard, "success"); } // ===== IMAP sync sources: shared helpers + delegated listeners ===== // Used by the mailbox/user/edit pages (each still owns its own draw_imapsync_source_table). // Kept here because 013-mailcow.js is loaded globally, so the logic lives in one place. // Human-readable visibility label for a source row (used by the per-page source tables) function imapsyncScopeDisplay(item) { if (item.scope === 'all') return ' ' + lang.syncjobs.source_scope_all; if (item.scope === 'domain') return escapeHtml(lang.syncjobs.source_scope_domain + ': ' + (item.domains || []).join(', ')); var us = item.users || []; if (us.length === 0) return escapeHtml(item.created_by || ''); return escapeHtml(lang.syncjobs.source_scope_user + ': ' + us.join(', ')); } // (Re)build every imapsync-source dropdown from the ACL-filtered API (fresh on modal open) function populateImapsyncSourceSelects() { $.get("/api/v1/get/syncjob_source/all", function(sources) { $('select.imapsync-source-select').each(function() { var $sel = $(this); var keepId = $sel.data('current-source-id'); $sel.empty(); $sel.append(''); $.each(sources, function(_, src) { var label = src.name + ' — ' + src.host1 + ':' + src.port1 + ' (' + src.auth_type + ')'; if (src.scope === 'all') label += ' [' + (lang.syncjobs ? lang.syncjobs.source_scope_all : 'all') + ']'; var $opt = $('').attr('value', name).text(name)); }); // keep a stored value that is no longer allowlisted visible instead of silently dropping it if (o && (imapsyncCpOptions || []).indexOf(o) === -1) { $sel.append($('').attr('value', o).text(o)); } $sel.val(o || ''); $row.find('.imapsync-cp-val').attr('placeholder', (lang.syncjobs && lang.syncjobs.custom_param_value) || 'value').val(v || ''); $editor.find('.imapsync-cp-rows').append($row); $sel.selectpicker(); } // Build the rows of an editor from its hidden JSON value. function imapsyncCpBuild($editor) { var $hidden = $editor.siblings('.imapsync-cp-value'); $editor.find('.imapsync-cp-opt').selectpicker('destroy'); $editor.find('.imapsync-cp-rows').empty(); var pairs = []; try { pairs = JSON.parse($hidden.val() || '[]'); } catch (e) { pairs = []; } if (!Array.isArray(pairs)) pairs = []; pairs.forEach(function(p) { if (p && typeof p === 'object') imapsyncCpAddRow($editor, p.o, p.v); }); } // Re-serialize an editor's rows back into its hidden JSON value (option name required). function imapsyncCpSerialize($editor) { var out = []; $editor.find('.imapsync-cp-row').each(function() { var o = $.trim($(this).find('.imapsync-cp-opt').val() || '').replace(/^-+/, ''); if (o === '') return; out.push({ o: o, v: $(this).find('.imapsync-cp-val').val() || '' }); }); $editor.siblings('.imapsync-cp-value').val(JSON.stringify(out)); } $(document).ready(function() { // Custom-params editor: keep the hidden JSON in sync with the rows $(document).on('change', '.imapsync-cp-opt', function() { imapsyncCpSerialize($(this).closest('.imapsync-cp-editor')); }); $(document).on('input', '.imapsync-cp-val', function() { imapsyncCpSerialize($(this).closest('.imapsync-cp-editor')); }); $(document).on('click', '.imapsync-cp-add', function() { var $editor = $(this).closest('.imapsync-cp-editor'); imapsyncCpEnsureOptions(function() { imapsyncCpAddRow($editor); }); }); $(document).on('click', '.imapsync-cp-remove', function() { var $editor = $(this).closest('.imapsync-cp-editor'); $(this).closest('.imapsync-cp-row').find('.imapsync-cp-opt').selectpicker('destroy'); $(this).closest('.imapsync-cp-row').remove(); imapsyncCpSerialize($editor); }); // Edit page (syncjob.twig): build rows from the stored value on load imapsyncCpEnsureOptions(function() { $('.imapsync-cp-editor').each(function() { imapsyncCpBuild($(this)); }); }); // Syncjob form: toggle password row / OAuth "connect" button by the selected source's flow $(document).on('change', 'select.imapsync-source-select', function() { var $sel = $(this); var $opt = $sel.find('option:selected'); var $form = $sel.closest('form'); var isOauth = ($opt.data('auth-type') === 'XOAUTH2'); var isAuthCode = isOauth && ($opt.data('oauth-flow') === 'authorization_code'); $form.find('.password1-row').toggle(!isOauth); $form.find('input[name="password1"]').prop('required', !isOauth); $form.find('.oauth-source-hint').toggle(isOauth); $form.find('.imapsync-oauth-connect-row').toggle(isAuthCode); }); // Source editor: OAuth block + authorization_code fields $(document).on('change', 'select.imapsync-source-auth-type, select.imapsync-source-oauth-flow', function() { imapsyncSyncSourceOauthToggle($(this).closest('form')); }); // Source editor: scope -> show + populate the domain/user pickers $(document).on('change', 'select.imapsync-source-scope', function() { var $form = $(this).closest('form'); var scope = $(this).val(); $form.find('.imapsync-source-domains-row').toggle(scope === 'domain'); $form.find('.imapsync-source-users-row').toggle(scope === 'user'); if (scope === 'domain') fillImapsyncScopeSelect($form.find('.imapsync-source-domains'), 'domain/all', 'domain_name'); if (scope === 'user') fillImapsyncScopeSelect($form.find('.imapsync-source-users'), 'mailbox/all', 'username'); }); // "Connect with the provider": open the per-user OAuth popup for the selected source $(document).on('click', '.imapsync-oauth-connect-btn', function() { var $form = $(this).closest('form'); var sourceId = $form.find('select.imapsync-source-select').val(); if (!sourceId) return; window.open('/syncjob-oauth?action=start&source_id=' + encodeURIComponent(sourceId), 'syncjob_oauth', 'width=600,height=700'); }); // Result from the OAuth popup: fill + lock user1, mark connected (or show the error) window.addEventListener('message', function(ev) { if (ev.origin !== window.location.origin || !ev.data || ev.data.type !== 'syncjob_oauth') return; $('.imapsync-oauth-connect-row:visible').each(function() { var $form = $(this).closest('form'); var $status = $form.find('.imapsync-oauth-connect-status'); if (ev.data.ok) { $form.find('input[name="user1"]').val(ev.data.user1).prop('readonly', true); $status.removeClass('text-danger').addClass('text-success') .text((lang.syncjobs ? lang.syncjobs.syncjob_oauth_connected : 'Connected') + ': ' + ev.data.user1); } else { $status.removeClass('text-success').addClass('text-danger').text(ev.data.error || 'error'); } }); }); // Add-source modal opens: init scope pickers + OAuth block (auth_type may be cached-form preselected) $(document).on('shown.bs.modal', '#addImapsyncSourceModal', function() { $(this).find('select.imapsync-source-scope').trigger('change'); imapsyncSyncSourceOauthToggle($(this).find('form')); }); // Add-syncjob modal opens: (re)load the source dropdown from the API $(document).on('shown.bs.modal', '#addSyncJobModalAdmin, #addSyncJobModal', function() { populateImapsyncSourceSelects(); var $modal = $(this); imapsyncCpEnsureOptions(function() { $modal.find('.imapsync-cp-editor').each(function() { imapsyncCpBuild($(this)); }); }); }); });