mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-28 20:23:26 +00:00
[Web][Dovecot][SOGo][DockerApi] Add mailbox groups, shared mailboxes and identity provider sync
This commit is contained in:
82 files changed
+6488
-1027
No files matched your search
@@ -934,6 +934,12 @@ jQuery(function($){
|
||||
$('#keycloak_settings').addClass('d-none');
|
||||
}
|
||||
});
|
||||
// LDAP: one select for the excluding use_ssl and use_tls (select only, bootstrap-select copies the class to its wrapper)
|
||||
$('select.iam_ldap_encryption').on('change', function() {
|
||||
var $form = $(this).closest('form');
|
||||
$form.find('input[name="use_ssl"]').val(this.value === 'ssl' ? 1 : 0);
|
||||
$form.find('input[name="use_tls"]').val(this.value === 'tls' ? 1 : 0);
|
||||
});
|
||||
function addAttributeMappingRow(list_id, del_class, e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Mailbox groups (admin/domainadmin mailbox page, group edit page). Loaded via $js_minifier.
|
||||
var mailcowGroups = (function($) {
|
||||
'use strict';
|
||||
|
||||
// Lang strings: lang.groups (mailbox page) or lang_groups (edit page), '' if missing
|
||||
function L(key) {
|
||||
var src = (typeof lang_groups !== 'undefined' && lang_groups) ? lang_groups :
|
||||
((typeof lang !== 'undefined' && lang && lang.groups) ? lang.groups : {});
|
||||
return (src[key] !== undefined && src[key] !== null) ? src[key] : '';
|
||||
}
|
||||
function esc(s) {
|
||||
return escapeHtml(s === null || s === undefined ? '' : s);
|
||||
}
|
||||
|
||||
function membersCell(members) {
|
||||
if (!Array.isArray(members) || members.length === 0) return '-';
|
||||
var list = members.slice(0, 3).map(function(m) {
|
||||
return (m.source === 'idp' ? '<i class="bi bi-cloud" title="' + esc(L('idp_member')) + '"></i> ' : '') + esc(m.username);
|
||||
}).join('<br>');
|
||||
if (members.length > 3) list += '<br><span class="text-muted">+' + (members.length - 3) + '</span>';
|
||||
return '<span class="badge bg-info">' + members.length + '</span><br>' + list;
|
||||
}
|
||||
|
||||
function addressCell(item) {
|
||||
if (!item.address) return '-';
|
||||
var flags = [];
|
||||
if (item.sender_allowed == 1) flags.push('<i class="bi bi-send" title="' + esc(L('sender_allowed')) + '"></i>');
|
||||
if (item.internal == 1) flags.push('<i class="bi bi-building-lock" title="' + esc(L('internal')) + '"></i>');
|
||||
return esc(item.address) + (flags.length ? ' ' + flags.join(' ') : '');
|
||||
}
|
||||
|
||||
function drawTable() {
|
||||
if ($.fn.DataTable.isDataTable('#mailbox_group_table')) {
|
||||
$('#mailbox_group_table').DataTable().columns.adjust().responsive.recalc();
|
||||
return;
|
||||
}
|
||||
$('#mailbox_group_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
order: [[3, 'asc']],
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/mailbox-group/all",
|
||||
dataSrc: function(json) {
|
||||
if (!Array.isArray(json)) return [];
|
||||
$.each(json, function(i, item) {
|
||||
item.chkbox = item.can_edit ?
|
||||
'<input type="checkbox" class="form-check-input" data-id="mailbox_group" name="multi_select" value="' + encodeURIComponent(item.id) + '" />' :
|
||||
'<input type="checkbox" class="form-check-input" disabled />';
|
||||
item.name_display = esc(item.name) + (item.idp_ref ? ' <span class="badge bg-info">' + esc(L('idp')) + '</span>' : '');
|
||||
if (item.description) item.name_display += '<div class="small text-muted">' + esc(item.description) + '</div>';
|
||||
item.address_display = addressCell(item);
|
||||
item.members_display = membersCell(item.members);
|
||||
item.send_as_display = (item.send_as || []).map(esc).join('<br>') || '-';
|
||||
item.template_display = item.template ? esc(item.template) + ' <span class="text-muted">(' + esc(item.priority) + ')</span>' : '-';
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="/edit/mailbox-group/' + encodeURIComponent(item.id) + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + esc(item.can_edit ? lang.edit : L('view')) + '</a>' +
|
||||
(item.can_edit ? '<a href="#" data-action="delete_selected" data-id="single-mailbox_group" data-api-url="delete/mailbox-group" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + esc(lang.remove) + '</a>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
return json;
|
||||
},
|
||||
error: function() {
|
||||
mailcow_alert_box(L('list_error'), 'danger');
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 1
|
||||
},
|
||||
{ title: '', data: 'chkbox', searchable: false, orderable: false, defaultContent: '', responsivePriority: 2 },
|
||||
{ title: 'ID', data: 'id', defaultContent: '', responsivePriority: 6 },
|
||||
{ title: L('name'), data: 'name_display', defaultContent: '', responsivePriority: 2 },
|
||||
{ title: L('domain'), data: 'domain', defaultContent: '', responsivePriority: 5 },
|
||||
{ title: L('address'), data: 'address_display', defaultContent: '', responsivePriority: 3 },
|
||||
{ title: L('members'), data: 'members_display', defaultContent: '', responsivePriority: 4 },
|
||||
{ title: L('send_as'), data: 'send_as_display', defaultContent: '', responsivePriority: 7 },
|
||||
{ title: L('template'), data: 'template_display', defaultContent: '', responsivePriority: 8 },
|
||||
{
|
||||
title: lang.active,
|
||||
data: 'active',
|
||||
defaultContent: '',
|
||||
responsivePriority: 6,
|
||||
render: function(d) { return d == 1 ? '<i class="bi bi-check-lg"></i>' : '<i class="bi bi-x-lg"></i>'; }
|
||||
},
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
orderable: false,
|
||||
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
||||
responsivePriority: 3,
|
||||
defaultContent: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Share rows (edit page, add modal) are kept as JSON in the hidden form field of their table (imap_grants, sogo_grants)
|
||||
function syncGrants($table) {
|
||||
var grants = [];
|
||||
$table.find('tr.group-grant').each(function() {
|
||||
var grant = {};
|
||||
$(this).find('[data-grant]').each(function() {
|
||||
grant[$(this).data('grant')] = $(this).is(':checkbox') ? ($(this).is(':checked') ? 1 : 0) : $(this).val();
|
||||
});
|
||||
if (grant.owner) grants.push(grant);
|
||||
});
|
||||
$table.closest('form').find('input[name="' + $table.data('field') + '"]').val(JSON.stringify(grants));
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// @domain suffix of the address in the add modal
|
||||
$('.mailbox-group-domain-select').on('change', function() {
|
||||
$(this).closest('form').find('.mailbox-group-domain').text($(this).val());
|
||||
});
|
||||
$(document).on('change', '.group-grants [data-grant]', function() {
|
||||
syncGrants($(this).closest('.group-grants'));
|
||||
});
|
||||
$(document).on('click', '.group-grant-add', function(e) {
|
||||
e.preventDefault();
|
||||
var $table = $('#' + $(this).data('table'));
|
||||
var $body = $table.children('tbody');
|
||||
if (!$body.length) $body = $('<tbody>').appendTo($table);
|
||||
var $row = $($('#' + $(this).data('table') + '_template').html()).appendTo($body);
|
||||
$row.find('select').selectpicker({'styleBase': 'btn btn-xs-lg', 'noneSelectedText': lang_footer.nothing_selected});
|
||||
syncGrants($table);
|
||||
});
|
||||
$(document).on('click', '.group-grant-del', function(e) {
|
||||
e.preventDefault();
|
||||
var $table = $(this).closest('.group-grants');
|
||||
$(this).closest('tr').remove();
|
||||
syncGrants($table);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
drawTable: drawTable,
|
||||
L: L
|
||||
};
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,104 @@
|
||||
// Identity provider sync: status card and run requests (admin system page, identity provider tab).
|
||||
// Runs are executed by the identity-sync cron (every minute); the card polls until the requested run is done.
|
||||
var mailcowIdentity = (function($) {
|
||||
'use strict';
|
||||
|
||||
var POLL_MS = 5000;
|
||||
var POLL_MAX = 36;
|
||||
var polls = 0;
|
||||
var timer = null;
|
||||
var STATUS_CLASS = {ok: 'bg-success', blocked: 'bg-warning text-dark', error: 'bg-danger', pending: 'bg-secondary', running: 'bg-info text-dark'};
|
||||
// summary counts in display order
|
||||
var COUNTS = ['creates', 'links', 'deactivate', 'restore', 'release', 'missing', 'found', 'updates', 'group_creates', 'group_renames',
|
||||
'memberships', 'orphaned_groups', 'membership_removals', 'conflicts', 'errors', 'skipped'];
|
||||
var DETAILS = ['conflicts', 'errors', 'deactivate', 'restore', 'missing', 'creates', 'group_creates'];
|
||||
|
||||
// admin.twig injects the admin lang section as `lang`
|
||||
function L(key) {
|
||||
var src = (typeof lang_admin !== 'undefined') ? lang_admin : ((typeof lang !== 'undefined') ? lang : {});
|
||||
return (src[key] !== undefined) ? src[key] : key;
|
||||
}
|
||||
function esc(s) {
|
||||
return escapeHtml(s === null || s === undefined ? '' : String(s));
|
||||
}
|
||||
|
||||
function badge(label, value, cls) {
|
||||
return '<span class="badge ' + (cls || 'bg-light text-dark border') + ' me-1 mb-1">' + esc(label) + ': ' + esc(value) + '</span>';
|
||||
}
|
||||
|
||||
function changes(run) {
|
||||
var s = run.summary;
|
||||
if (!s) return '';
|
||||
if (s.error) return '<span class="text-danger">' + esc(s.error) + '</span>';
|
||||
var counts = s.counts || {};
|
||||
var html = '';
|
||||
$.each(COUNTS, function(i, key) {
|
||||
if (counts[key] > 0) html += badge(L('iam_sync_count_' + key), counts[key], (key === 'errors' || key === 'conflicts') ? 'bg-danger' : null);
|
||||
});
|
||||
if (html === '') html = '<span class="text-muted">' + esc(L('iam_sync_no_changes')) + '</span>';
|
||||
var details = $.extend({}, s.details || {}, {errors: s.errors || []});
|
||||
var lists = '';
|
||||
$.each(DETAILS, function(i, key) {
|
||||
var items = details[key] || [];
|
||||
if (!items.length) return;
|
||||
lists += '<div class="mt-1"><b>' + esc(L('iam_sync_count_' + key)) + '</b><ul class="mb-1">' +
|
||||
$.map(items, function(item) { return '<li>' + esc(item) + '</li>'; }).join('') + '</ul></div>';
|
||||
});
|
||||
if (lists !== '') html += '<details class="small mt-1"><summary>' + esc(L('iam_sync_details')) + '</summary>' + lists + '</details>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function render(status) {
|
||||
var c = status.counts || {};
|
||||
var counts = badge(L('iam_sync_accounts'), c.accounts || 0) + badge(L('iam_sync_linked'), c.linked || 0) +
|
||||
badge(L('iam_sync_idp_disabled'), c.idp_disabled || 0, c.idp_disabled ? 'bg-warning text-dark' : null) +
|
||||
badge(L('iam_sync_missing'), c.missing || 0, c.missing ? 'bg-warning text-dark' : null);
|
||||
if (!status.sync_enabled) counts += '<div class="text-muted small mt-1">' + esc(L('iam_sync_off')) + '</div>';
|
||||
$('#iam_sync_counts').html(counts);
|
||||
var rows = $.map(status.runs || [], function(run) {
|
||||
var state = '<span class="badge ' + (STATUS_CLASS[run.status] || 'bg-secondary') + '">' + esc(L('iam_sync_status_' + run.status)) + '</span>';
|
||||
return '<tr><td class="text-nowrap">' + esc(run.started || run.created) + '</td>' +
|
||||
'<td>' + esc(L('iam_sync_mode_' + run.mode)) + (run.requested_by ? '<div class="small text-muted">' + esc(run.requested_by) + '</div>' : '') + '</td>' +
|
||||
'<td>' + state + '</td><td>' + changes(run) + '</td></tr>';
|
||||
});
|
||||
$('#iam_sync_runs tbody').html(rows.length ? rows.join('') : '<tr><td colspan="4" class="text-muted">' + esc(L('iam_sync_none')) + '</td></tr>');
|
||||
return status.runs && status.runs.length && (status.runs[0].status === 'pending' || status.runs[0].status === 'running');
|
||||
}
|
||||
|
||||
function load() {
|
||||
clearTimeout(timer);
|
||||
$.get('/api/v1/get/identity-provider/sync-status', function(status) {
|
||||
if (render(status || {}) && polls++ < POLL_MAX) timer = setTimeout(load, POLL_MS);
|
||||
}, 'json');
|
||||
}
|
||||
|
||||
function request(mode) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
url: '/api/v1/edit/identity-provider-sync',
|
||||
data: {items: JSON.stringify(['sync']), attr: JSON.stringify({mode: mode}), csrf_token: csrf_token},
|
||||
success: function(res) {
|
||||
var msg = (res && res[0]) ? res[0] : {type: 'danger', msg: 'generic_server_error'};
|
||||
var key = $.isArray(msg.msg) ? msg.msg[0] : msg.msg;
|
||||
var text = (msg.type === 'success' ? lang_success : lang_danger)[key] || key;
|
||||
mailcow_alert_box(text, msg.type === 'success' ? 'success' : 'danger');
|
||||
polls = 0;
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
if (!$('#iam_sync_status').length) return;
|
||||
load();
|
||||
$(document).on('click', '.iam_sync_request', function(e) {
|
||||
e.preventDefault();
|
||||
var mode = $(this).data('mode');
|
||||
if (mode === 'force' && !confirm(L('iam_sync_force_confirm'))) return;
|
||||
request(mode);
|
||||
});
|
||||
});
|
||||
|
||||
return {load: load};
|
||||
})(jQuery);
|
||||
@@ -120,6 +120,10 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
// Shared mailboxes need no password, the server sets a random one
|
||||
$('#add_mailbox_shared').on('change', function() {
|
||||
$(this).closest('form').find('input[name="password"], input[name="password2"]').prop('required', !this.checked);
|
||||
});
|
||||
// @Open Mailbox add modal
|
||||
$('#addMailboxModal').on('show.bs.modal', function(e) {
|
||||
$.ajax({
|
||||
@@ -996,6 +1000,9 @@ jQuery(function($){
|
||||
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'
|
||||
};
|
||||
item.username = escapeHtml(item.username);
|
||||
if (item.attributes && item.attributes.shared == '1') {
|
||||
item.username += ' <span class="badge bg-secondary">' + escapeHtml(lang.shared_mailbox) + '</span>';
|
||||
}
|
||||
item.name = escapeHtml(item.name);
|
||||
|
||||
if (Array.isArray(item.tags)){
|
||||
@@ -1926,6 +1933,12 @@ jQuery(function($){
|
||||
'<a href="#" data-action="delete_selected" data-id="single-alias" data-api-url="delete/alias" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
|
||||
if (item.group_id) {
|
||||
// Distribution address of a group: changed through the group
|
||||
item.action = '<a href="/edit/mailbox-group/' + encodeURIComponent(item.group_id) + '" class="btn btn-sm btn-xs-lg btn-secondary"><i class="bi bi-people"></i> ' +
|
||||
escapeHtml((lang.groups && lang.groups.managed_alias ? lang.groups.managed_alias : '%s').replace('%s', item.group_name)) + '</a>';
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" disabled />';
|
||||
}
|
||||
item.goto = escapeHtml(item.goto.replace(/,/g, " "));
|
||||
if (item.public_comment !== null) {
|
||||
item.public_comment = escapeHtml(item.public_comment);
|
||||
@@ -2479,6 +2492,7 @@ jQuery(function($){
|
||||
onVisible("[id^=aliasdomain_table]", () => draw_aliasdomain_table());
|
||||
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
|
||||
onVisible("[id^=imapsync_source_table]", () => mailcowImapsync.drawSourceTable(role === 'admin' || acl_data.syncjobs === 1));
|
||||
onVisible("[id^=mailbox_group_table]", () => mailcowGroups.drawTable());
|
||||
onVisible("[id^=filter_table]", () => draw_filter_table());
|
||||
onVisible("[id^=bcc_table]", () => draw_bcc_table());
|
||||
onVisible("[id^=recipient_map_table]", () => draw_recipient_map_table());
|
||||
|
||||
Reference in new issue
Block a user