mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-04-15 18:18:51 +00:00
Compare commits
3 Commits
renovate/p
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f924b7f0e3 | ||
|
|
9610a79c3e | ||
|
|
886dbcc419 |
@@ -69,6 +69,8 @@ RUN addgroup -g 5000 vmail \
|
||||
perl-par-packer \
|
||||
perl-parse-recdescent \
|
||||
perl-lockfile-simple \
|
||||
perl-parallel-forkmanager \
|
||||
perl-redis \
|
||||
libproc2 \
|
||||
perl-readonly \
|
||||
perl-regexp-common \
|
||||
|
||||
@@ -2,21 +2,15 @@
|
||||
|
||||
use DBI;
|
||||
use LockFile::Simple qw(lock trylock unlock);
|
||||
use Proc::ProcessTable;
|
||||
use Data::Dumper qw(Dumper);
|
||||
use IPC::Run 'run';
|
||||
use File::Temp;
|
||||
use Try::Tiny;
|
||||
use Parallel::ForkManager;
|
||||
use Redis;
|
||||
use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
|
||||
|
||||
sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s };
|
||||
my $t = Proc::ProcessTable->new;
|
||||
my $imapsync_running = grep { $_->{cmndline} =~ /imapsync\s/i } @{$t->table};
|
||||
if ($imapsync_running ge 1)
|
||||
{
|
||||
print "imapsync is active, exiting...";
|
||||
exit;
|
||||
}
|
||||
|
||||
sub qqw($) {
|
||||
my @params = ();
|
||||
@@ -35,93 +29,51 @@ sub qqw($) {
|
||||
return @params;
|
||||
}
|
||||
|
||||
$run_dir="/tmp";
|
||||
$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock';
|
||||
$lock_file = $run_dir . "/imapsync_busy";
|
||||
$lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1);
|
||||
$lockmgr->lock($lock_file) || die "can't lock ${lock_file}";
|
||||
$dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
mysql_auto_reconnect => 1,
|
||||
mysql_enable_utf8mb4 => 1
|
||||
});
|
||||
$dbh->do("UPDATE imapsync SET is_running = 0");
|
||||
our $pm;
|
||||
|
||||
sub sig_handler {
|
||||
# Send die to force exception in "run"
|
||||
if (defined $pm) {
|
||||
foreach my $child_pid (keys %{ $pm->{processes} }) {
|
||||
kill 'TERM', $child_pid;
|
||||
}
|
||||
}
|
||||
die "sig_handler received signal, preparing to exit...\n";
|
||||
};
|
||||
|
||||
open my $file, '<', "/etc/sogo/sieve.creds";
|
||||
my $creds = <$file>;
|
||||
close $file;
|
||||
my ($master_user, $master_pass) = split /:/, $creds;
|
||||
my $sth = $dbh->prepare("SELECT id,
|
||||
user1,
|
||||
user2,
|
||||
host1,
|
||||
authmech1,
|
||||
password1,
|
||||
exclude,
|
||||
port1,
|
||||
enc1,
|
||||
delete2duplicates,
|
||||
maxage,
|
||||
subfolder2,
|
||||
delete1,
|
||||
delete2,
|
||||
automap,
|
||||
skipcrossduplicates,
|
||||
maxbytespersecond,
|
||||
custom_params,
|
||||
subscribeall,
|
||||
timeout1,
|
||||
timeout2,
|
||||
dry
|
||||
FROM imapsync
|
||||
WHERE active = 1
|
||||
AND is_running = 0
|
||||
AND (
|
||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60
|
||||
OR
|
||||
last_run IS NULL)
|
||||
ORDER BY last_run");
|
||||
sub run_one_job {
|
||||
my ($dbh, $row_ref, $master_user, $master_pass) = @_;
|
||||
|
||||
$sth->execute();
|
||||
my $row;
|
||||
|
||||
while ($row = $sth->fetchrow_arrayref()) {
|
||||
|
||||
$id = @$row[0];
|
||||
$user1 = @$row[1];
|
||||
$user2 = @$row[2];
|
||||
$host1 = @$row[3];
|
||||
$authmech1 = @$row[4];
|
||||
$password1 = @$row[5];
|
||||
$exclude = @$row[6];
|
||||
$port1 = @$row[7];
|
||||
$enc1 = @$row[8];
|
||||
$delete2duplicates = @$row[9];
|
||||
$maxage = @$row[10];
|
||||
$subfolder2 = @$row[11];
|
||||
$delete1 = @$row[12];
|
||||
$delete2 = @$row[13];
|
||||
$automap = @$row[14];
|
||||
$skipcrossduplicates = @$row[15];
|
||||
$maxbytespersecond = @$row[16];
|
||||
$custom_params = @$row[17];
|
||||
$subscribeall = @$row[18];
|
||||
$timeout1 = @$row[19];
|
||||
$timeout2 = @$row[20];
|
||||
$dry = @$row[21];
|
||||
my $id = $row_ref->[0];
|
||||
my $user1 = $row_ref->[1];
|
||||
my $user2 = $row_ref->[2];
|
||||
my $host1 = $row_ref->[3];
|
||||
my $authmech1 = $row_ref->[4];
|
||||
my $password1 = $row_ref->[5];
|
||||
my $exclude = $row_ref->[6];
|
||||
my $port1 = $row_ref->[7];
|
||||
my $enc1 = $row_ref->[8];
|
||||
my $delete2duplicates = $row_ref->[9];
|
||||
my $maxage = $row_ref->[10];
|
||||
my $subfolder2 = $row_ref->[11];
|
||||
my $delete1 = $row_ref->[12];
|
||||
my $delete2 = $row_ref->[13];
|
||||
my $automap = $row_ref->[14];
|
||||
my $skipcrossduplicates = $row_ref->[15];
|
||||
my $maxbytespersecond = $row_ref->[16];
|
||||
my $custom_params = $row_ref->[17];
|
||||
my $subscribeall = $row_ref->[18];
|
||||
my $timeout1 = $row_ref->[19];
|
||||
my $timeout2 = $row_ref->[20];
|
||||
my $dry = $row_ref->[21];
|
||||
|
||||
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
||||
|
||||
my $template = $run_dir . '/imapsync.XXXXXXX';
|
||||
my $template = "/tmp/imapsync.XXXXXXX";
|
||||
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
||||
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
||||
|
||||
|
||||
binmode( $passfile1, ":utf8" );
|
||||
|
||||
|
||||
print $passfile1 "$password1\n";
|
||||
print $passfile2 trim($master_pass) . "\n";
|
||||
|
||||
@@ -157,40 +109,130 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
'--noreleasecheck'];
|
||||
|
||||
try {
|
||||
$is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?");
|
||||
$is_running->bind_param( 1, ${id} );
|
||||
my $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?");
|
||||
$is_running->bind_param( 1, $id );
|
||||
$is_running->execute();
|
||||
|
||||
run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout;
|
||||
|
||||
# check exit code and status
|
||||
($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/);
|
||||
my ($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/);
|
||||
|
||||
$success = 0;
|
||||
my $success = 0;
|
||||
if (defined $exit_code && $exit_code == 0) {
|
||||
$success = 1;
|
||||
}
|
||||
|
||||
$update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?");
|
||||
$update->bind_param( 1, ${stdout} );
|
||||
$update->bind_param( 2, ${success} );
|
||||
$update->bind_param( 3, ${exit_status} );
|
||||
$update->bind_param( 4, ${id} );
|
||||
my $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?");
|
||||
$update->bind_param( 1, $stdout );
|
||||
$update->bind_param( 2, $success );
|
||||
$update->bind_param( 3, $exit_status );
|
||||
$update->bind_param( 4, $id );
|
||||
$update->execute();
|
||||
} catch {
|
||||
$update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?");
|
||||
$update->bind_param( 1, ${id} );
|
||||
my $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?");
|
||||
$update->bind_param( 1, $id );
|
||||
$update->execute();
|
||||
} finally {
|
||||
$update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?");
|
||||
$update->bind_param( 1, ${id} );
|
||||
my $update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?");
|
||||
$update->bind_param( 1, $id );
|
||||
$update->execute();
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
my $run_dir = "/tmp";
|
||||
my $dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock';
|
||||
my $lock_file = $run_dir . "/imapsync_busy";
|
||||
my $lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1);
|
||||
$lockmgr->lock($lock_file) || die "can't lock ${lock_file}";
|
||||
|
||||
my $max_parallel = 1;
|
||||
try {
|
||||
my $redis = Redis->new(
|
||||
server => 'redis-mailcow:6379',
|
||||
password => $ENV{'REDISPASS'},
|
||||
reconnect => 10,
|
||||
every => 1_000_000,
|
||||
);
|
||||
my $val = $redis->get('SYNCJOBS_MAX_PARALLEL');
|
||||
if (defined $val && $val =~ /^\d+$/) {
|
||||
$max_parallel = int($val);
|
||||
}
|
||||
$redis->quit();
|
||||
} catch {
|
||||
warn "Could not read SYNCJOBS_MAX_PARALLEL from Redis, defaulting to 1: $_";
|
||||
};
|
||||
$max_parallel = 1 if $max_parallel < 1;
|
||||
$max_parallel = 50 if $max_parallel > 50;
|
||||
|
||||
my $dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
mysql_auto_reconnect => 1,
|
||||
mysql_enable_utf8mb4 => 1
|
||||
});
|
||||
$dbh->do("UPDATE imapsync SET is_running = 0");
|
||||
|
||||
open my $file, '<', "/etc/sogo/sieve.creds";
|
||||
my $creds = <$file>;
|
||||
close $file;
|
||||
my ($master_user, $master_pass) = split /:/, $creds;
|
||||
|
||||
my $sth = $dbh->prepare("SELECT id,
|
||||
user1,
|
||||
user2,
|
||||
host1,
|
||||
authmech1,
|
||||
password1,
|
||||
exclude,
|
||||
port1,
|
||||
enc1,
|
||||
delete2duplicates,
|
||||
maxage,
|
||||
subfolder2,
|
||||
delete1,
|
||||
delete2,
|
||||
automap,
|
||||
skipcrossduplicates,
|
||||
maxbytespersecond,
|
||||
custom_params,
|
||||
subscribeall,
|
||||
timeout1,
|
||||
timeout2,
|
||||
dry
|
||||
FROM imapsync
|
||||
WHERE active = 1
|
||||
AND is_running = 0
|
||||
AND (
|
||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60
|
||||
OR
|
||||
last_run IS NULL)
|
||||
ORDER BY last_run");
|
||||
|
||||
$sth->execute();
|
||||
my @jobs;
|
||||
while (my $row = $sth->fetchrow_arrayref()) {
|
||||
push @jobs, [ @$row ];
|
||||
}
|
||||
$sth->finish();
|
||||
$dbh->disconnect();
|
||||
|
||||
$pm = Parallel::ForkManager->new($max_parallel);
|
||||
|
||||
JOB:
|
||||
foreach my $job (@jobs) {
|
||||
my $pid = $pm->start;
|
||||
if ($pid) {
|
||||
next JOB;
|
||||
}
|
||||
|
||||
my $child_dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
mysql_auto_reconnect => 1,
|
||||
mysql_enable_utf8mb4 => 1
|
||||
});
|
||||
run_one_job($child_dbh, $job, $master_user, $master_pass);
|
||||
$child_dbh->disconnect();
|
||||
|
||||
$pm->finish;
|
||||
}
|
||||
|
||||
$pm->wait_all_children;
|
||||
|
||||
$lockmgr->unlock($lock_file);
|
||||
|
||||
@@ -106,6 +106,7 @@ $template_data = [
|
||||
'f2b_data' => $f2b_data,
|
||||
'f2b_banlist_url' => getBaseUrl() . "/f2b-banlist?id=" . $f2b_data['banlist_id'],
|
||||
'q_data' => quarantine('settings'),
|
||||
'sj_data' => mailbox('get', 'syncjob_settings'),
|
||||
'qn_data' => quota_notification('get'),
|
||||
'pw_reset_data' => reset_password('get_notification'),
|
||||
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
||||
|
||||
@@ -3931,6 +3931,64 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Update sync job
|
||||
/api/v1/edit/syncjob_settings:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
log:
|
||||
- entity
|
||||
- action
|
||||
- object
|
||||
msg:
|
||||
- message
|
||||
- entity name
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Update the global sync job settings. Currently exposes the maximum
|
||||
number of imapsync processes that are allowed to run in parallel.
|
||||
Admin access required.
|
||||
operationId: Update sync job settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
max_parallel: 4
|
||||
properties:
|
||||
max_parallel:
|
||||
description: >-
|
||||
Maximum number of imapsync processes allowed to run in
|
||||
parallel (1 = sequential behavior, max 50)
|
||||
type: integer
|
||||
type: object
|
||||
summary: Update sync job settings
|
||||
/api/v1/edit/user-acl:
|
||||
post:
|
||||
responses:
|
||||
@@ -5629,6 +5687,36 @@ paths:
|
||||
description: You can list all syn jobs existing in system.
|
||||
operationId: Get sync jobs
|
||||
summary: Get sync jobs
|
||||
/api/v1/get/syncjob_settings:
|
||||
get:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
max_parallel: 4
|
||||
schema:
|
||||
properties:
|
||||
max_parallel:
|
||||
description: >-
|
||||
Maximum number of imapsync processes allowed to run in
|
||||
parallel (1 = sequential behavior)
|
||||
type: integer
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Return the global sync job settings. Currently exposes the maximum
|
||||
number of imapsync processes allowed to run in parallel. Admin access
|
||||
required.
|
||||
operationId: Get sync job settings
|
||||
summary: Get sync job settings
|
||||
"/api/v1/get/tls-policy-map/{id}":
|
||||
get:
|
||||
parameters:
|
||||
|
||||
@@ -2265,6 +2265,35 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'syncjob_settings':
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$max_parallel = intval($_data['max_parallel']);
|
||||
if ($max_parallel < 1) { $max_parallel = 1; }
|
||||
if ($max_parallel > 50) { $max_parallel = 50; }
|
||||
try {
|
||||
$redis->Set('SYNCJOBS_MAX_PARALLEL', $max_parallel);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('redis_error', $e->getMessage())
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'syncjob_settings_saved'
|
||||
);
|
||||
break;
|
||||
case 'syncjob':
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
@@ -4619,6 +4648,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
return $syncjobdetails;
|
||||
break;
|
||||
case 'syncjob_settings':
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
return false;
|
||||
}
|
||||
$settings = array();
|
||||
try {
|
||||
$max_parallel = $redis->Get('SYNCJOBS_MAX_PARALLEL');
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$max_parallel = null;
|
||||
}
|
||||
$settings['max_parallel'] = intval($max_parallel) ?: 1;
|
||||
return $settings;
|
||||
break;
|
||||
case 'syncjobs':
|
||||
$syncjobdata = array();
|
||||
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
|
||||
|
||||
@@ -2150,7 +2150,7 @@ jQuery(function($){
|
||||
var table = $('#sync_job_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
@@ -2162,10 +2162,15 @@ jQuery(function($){
|
||||
hideTableExpandCollapseBtn('#tab-syncjobs', '#sync_job_table');
|
||||
},
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/syncjobs/all/no_log",
|
||||
type: "POST",
|
||||
url: "/api/v1/search/syncjob",
|
||||
contentType: "application/json",
|
||||
processData: false,
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
},
|
||||
dataSrc: function(json){
|
||||
$.each(json, function (i, item) {
|
||||
$.each(json.data, function (i, item) {
|
||||
item.log = '<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + encodeURIComponent(item.id) + '">' + lang.open_logs + '</a>'
|
||||
item.user2 = escapeHtml(item.user2);
|
||||
if (!item.exclude > 0) {
|
||||
@@ -2201,7 +2206,7 @@ jQuery(function($){
|
||||
item.exit_status = item.success + ' ' + item.exit_status;
|
||||
});
|
||||
|
||||
return json;
|
||||
return json.data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
@@ -2225,6 +2230,7 @@ jQuery(function($){
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'id',
|
||||
searchable: false,
|
||||
responsivePriority: 3,
|
||||
defaultContent: ''
|
||||
},
|
||||
@@ -2242,21 +2248,27 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.last_run,
|
||||
data: 'last_run',
|
||||
searchable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.syncjob_last_run_result,
|
||||
data: 'exit_status',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Log',
|
||||
data: 'log',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.active,
|
||||
data: 'active',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
render: function (data, type) {
|
||||
return 1==data?'<i class="bi bi-check-lg"><span class="sorting-value">1</span></i>':0==data&&'<i class="bi bi-x-lg"><span class="sorting-value">0</span></i>';
|
||||
@@ -2265,23 +2277,31 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.status,
|
||||
data: 'is_running',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.excludes,
|
||||
data: 'exclude',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: lang.mins_interval,
|
||||
data: 'mins_interval',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: '',
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
|
||||
responsivePriority: 5,
|
||||
defaultContent: ''
|
||||
|
||||
@@ -1104,6 +1104,10 @@ if (isset($_GET['query'])) {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "syncjob_settings":
|
||||
$data = mailbox('get', 'syncjob_settings');
|
||||
process_get_return($data);
|
||||
break;
|
||||
case "active-user-sieve":
|
||||
if (isset($object)) {
|
||||
$sieve_filter = mailbox('get', 'active_user_sieve', $object);
|
||||
@@ -1672,6 +1676,55 @@ if (isset($_GET['query'])) {
|
||||
process_search_return($data);
|
||||
break;
|
||||
|
||||
case "syncjob":
|
||||
$table = ['imapsync', 'i'];
|
||||
$primaryKey = 'id';
|
||||
$columns = [
|
||||
['db' => 'id', 'dt' => 2],
|
||||
['db' => 'user2', 'dt' => 3],
|
||||
['db' => 'host1', 'dt' => 4, 'search' => ['where_column' => "CONCAT_WS(' ', `i`.`user1`, `i`.`host1`)"]],
|
||||
['db' => 'last_run', 'dt' => 5],
|
||||
['db' => 'active', 'dt' => 8],
|
||||
];
|
||||
|
||||
if ($_SESSION['mailcow_cc_role'] === 'admin') {
|
||||
$data = SSP::simple($requestDecoded, $pdo, $table, $primaryKey, $columns);
|
||||
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
|
||||
$data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns,
|
||||
'INNER JOIN `mailbox` AS `mb` ON `mb`.`username` = `i`.`user2` ' .
|
||||
'INNER JOIN `domain_admins` AS `da` ON `da`.`domain` = `mb`.`domain`',
|
||||
[
|
||||
'condition' => '`da`.`active` = 1 AND `da`.`username` = :username',
|
||||
'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
|
||||
]);
|
||||
} elseif ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||
$data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns, null,
|
||||
[
|
||||
'condition' => '`i`.`user2` = :username',
|
||||
'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(403);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'Insufficient permissions'
|
||||
));
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!empty($data['data'])) {
|
||||
$syncjobData = [];
|
||||
foreach ($data['data'] as $row) {
|
||||
if ($details = mailbox('get', 'syncjob_details', $row[2], array('no_log'))) {
|
||||
$syncjobData[] = $details;
|
||||
}
|
||||
}
|
||||
$data['data'] = $syncjobData;
|
||||
}
|
||||
|
||||
process_search_return($data);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(array(
|
||||
@@ -1970,6 +2023,9 @@ if (isset($_GET['query'])) {
|
||||
case "syncjob":
|
||||
process_edit_return(mailbox('edit', 'syncjob', array_merge(array('id' => $items), $attr)));
|
||||
break;
|
||||
case "syncjob_settings":
|
||||
process_edit_return(mailbox('edit', 'syncjob_settings', $attr));
|
||||
break;
|
||||
case "filter":
|
||||
process_edit_return(mailbox('edit', 'filter', array_merge(array('id' => $items), $attr)));
|
||||
break;
|
||||
|
||||
5
data/web/lang/lang.az-az.json
Normal file
5
data/web/lang/lang.az-az.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"acl": {
|
||||
"login_as": "E-poçt qutusu istifadəçisi olaraq daxil ol"
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,8 @@
|
||||
"spamfilter": "Spamfilter",
|
||||
"subject": "Betreff",
|
||||
"success": "Erfolg",
|
||||
"syncjobs": "Sync-Jobs",
|
||||
"syncjobs_max_parallel": "Maximale Anzahl paralleler Sync-Jobs<br><small>Wie viele imapsync-Prozesse dürfen gleichzeitig laufen. 1 = sequentiell (aktuelles Verhalten).</small>",
|
||||
"sys_mails": "System-E-Mails",
|
||||
"task": "Aufgabe",
|
||||
"text": "Text",
|
||||
@@ -1200,6 +1202,7 @@
|
||||
"template_modified": "Änderungen am Template %s wurden gespeichert",
|
||||
"template_removed": "Template ID %s wurde gelöscht",
|
||||
"sogo_profile_reset": "ActiveSync-Gerät des Benutzers %s wurde zurückgesetzt",
|
||||
"syncjob_settings_saved": "Sync-Job-Einstellungen wurden gespeichert",
|
||||
"tls_policy_map_entry_deleted": "TLS-Richtlinie mit der ID %s wurde gelöscht",
|
||||
"tls_policy_map_entry_saved": "TLS-Richtlinieneintrag \"%s\" wurde gespeichert",
|
||||
"ui_texts": "Änderungen an UI-Texten",
|
||||
|
||||
@@ -382,6 +382,8 @@
|
||||
"spamfilter": "Spam filter",
|
||||
"subject": "Subject",
|
||||
"success": "Success",
|
||||
"syncjobs": "Sync jobs",
|
||||
"syncjobs_max_parallel": "Maximum parallel sync jobs<br><small>How many imapsync processes are allowed to run in parallel. 1 = sequential (legacy behavior).</small>",
|
||||
"sys_mails": "System mails",
|
||||
"task": "Task",
|
||||
"text": "Text",
|
||||
@@ -1204,6 +1206,7 @@
|
||||
"settings_map_added": "Added settings map entry",
|
||||
"settings_map_removed": "Removed settings map ID %s",
|
||||
"sogo_profile_reset": "SOGo profile for user %s was reset",
|
||||
"syncjob_settings_saved": "Sync job settings have been saved",
|
||||
"template_added": "Added template %s",
|
||||
"template_modified": "Changes to template %s have been saved",
|
||||
"template_removed": "Template ID %s has been deleted",
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"validation_success": "Validado com sucesso",
|
||||
"dry": "Simular sincronização",
|
||||
"internal": "Interno",
|
||||
"internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou alias de domínio."
|
||||
"internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou alias de domínio.",
|
||||
"sender_allowed": "Permitir enviar como este alias"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Acesso",
|
||||
@@ -151,7 +152,7 @@
|
||||
"arrival_time": "Hora de chegada (hora do servidor)",
|
||||
"authed_user": "Usuário autoritário",
|
||||
"ays": "Tem certeza de que deseja continuar?",
|
||||
"ban_list_info": "Veja uma lista de IPs banidos abaixo: <b>rede (tempo restante de banimento) - [ações]</b>. <br />Os IPs na fila para serem desbanidos serão removidos da lista de banimentos ativos em alguns segundos. <br />Rótulos vermelhos indicam proibições permanentes ativas na lista negra.",
|
||||
"ban_list_info": "Veja abaixo a lista de IPs banidos: <b>rede (tempo restante do banimento) - [ações]</b>.<br />IPs na fila para serem desbanidos serão removidos da lista de banimentos ativos em alguns segundos.<br />Rótulos vermelhos indicam banimentos permanentes ativos na lista de bloqueio.",
|
||||
"change_logo": "Alterar logotipo",
|
||||
"logo_normal_label": "Normal",
|
||||
"logo_dark_label": "Invertido para o modo escuro",
|
||||
@@ -188,9 +189,9 @@
|
||||
"excludes": "Exclui esses destinatários",
|
||||
"f2b_ban_time": "Tempo (s) de proibição",
|
||||
"f2b_ban_time_increment": "O tempo de banimento é incrementado com cada banimento",
|
||||
"f2b_blacklist": "Redes/hosts na lista negra",
|
||||
"f2b_blacklist": "Redes/hosts na lista de bloqueio",
|
||||
"f2b_filter": "Filtros Regex",
|
||||
"f2b_list_info": "Um host ou rede na lista negra sempre superará uma entidade na lista branca. <b>As atualizações da lista levarão alguns segundos para serem aplicadas.</b>",
|
||||
"f2b_list_info": "Um host ou rede na lista de bloqueio sempre terá prioridade sobre uma entidade na lista de permissões. <b>As atualizações das listas levarão alguns segundos para serem aplicadas.</b>",
|
||||
"f2b_manage_external": "Gerenciar Fail2Ban externamente",
|
||||
"f2b_manage_external_info": "O Fail2ban ainda manterá a lista de banimentos, mas não definirá ativamente regras para bloquear o tráfego. Use a lista de banimento gerada abaixo para bloquear externamente o tráfego.",
|
||||
"f2b_max_attempts": "Máximo de tentativas",
|
||||
@@ -200,7 +201,7 @@
|
||||
"f2b_parameters": "Parâmetros do Fail2ban",
|
||||
"f2b_regex_info": "Registros considerados: SoGo, Postfix, Dovecot, PHP-FPM.",
|
||||
"f2b_retry_window": "Repita a (s) janela (s) para o máximo de tentativas",
|
||||
"f2b_whitelist": "Redes/hosts incluídos na lista branca",
|
||||
"f2b_whitelist": "Redes/hosts na lista de permissões",
|
||||
"filter_table": "Tabela de filtros",
|
||||
"forwarding_hosts": "Anfitriões de encaminhamento",
|
||||
"forwarding_hosts_add_hint": "Você pode especificar endereços IPv4/IPv6, redes em notação CIDR, nomes de host (que serão resolvidos para endereços IP) ou nomes de domínio (que serão resolvidos para endereços IP consultando registros SPF ou, na ausência deles, registros MX).",
|
||||
@@ -320,8 +321,8 @@
|
||||
"rspamd_com_settings": "Um nome de configuração será gerado automaticamente, veja os exemplos de predefinições abaixo. Para obter mais detalhes, consulte a documentação <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">do Rspamd</a>",
|
||||
"rspamd_global_filters": "Mapas de filtro globais",
|
||||
"rspamd_global_filters_agree": "Eu vou ter cuidado!",
|
||||
"rspamd_global_filters_info": "Os mapas de filtros globais contêm diferentes tipos de listas negras e brancas globais.",
|
||||
"rspamd_global_filters_regex": "Seus nomes explicam seu propósito. <code>Todo o conteúdo deve conter uma expressão regular válida no formato “/padrão/opções” (por exemplo, /. + @domain\\ .tld/i</code>). <br>\r\n Embora verificações rudimentares estejam sendo executadas em cada linha de regex, a funcionalidade do Rspamd pode ser interrompida se não conseguir ler a sintaxe corretamente. <br>\r\n O Rspamd tentará ler o conteúdo do mapa quando alterado. Se você tiver problemas, <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">reinicie o Rspamd</a> para forçar o recarregamento do mapa. <br>Os elementos da lista negra são excluídos da quarentena.",
|
||||
"rspamd_global_filters_info": "Os mapas de filtros globais contêm diferentes tipos de listas globais de bloqueio e de permissões.",
|
||||
"rspamd_global_filters_regex": "Os nomes explicam sua finalidade. Todo o conteúdo deve conter uma expressão regular válida no formato \"/padrão/opções\" (por exemplo, <code>/.[+@domain.tld](mailto:+@domain.tld)/i</code>).<br>\nEmbora verificações básicas sejam executadas em cada linha de regex, a funcionalidade do Rspamd pode ser comprometida se ele não conseguir ler a sintaxe corretamente.<br>\nO Rspamd tentará ler o conteúdo do mapa quando ele for alterado. Se você tiver problemas, <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">reinicie o Rspamd</a> para forçar o recarregamento do mapa.<br>Elementos na lista de bloqueio são excluídos da quarentena.",
|
||||
"rspamd_settings_map": "Mapa de configurações do Rspamd",
|
||||
"sal_level": "Nível de humor",
|
||||
"save": "Salvar alterações",
|
||||
@@ -556,7 +557,9 @@
|
||||
"mode_invalid": "Modo %s é inválido",
|
||||
"mx_invalid": "Registro MX %s é inválido",
|
||||
"required_data_missing": "Dados obrigatórios %s estão ausentes",
|
||||
"version_invalid": "Versão %s é inválida"
|
||||
"version_invalid": "Versão %s é inválida",
|
||||
"tfa_removal_blocked": "A autenticação de dois fatores não pode ser removida, pois é obrigatória para a sua conta.",
|
||||
"quarantine_category_invalid": "A categoria da quarentena deve ser uma das seguintes: add_header, reject, all"
|
||||
},
|
||||
"datatables": {
|
||||
"collapse_all": "Recolher tudo",
|
||||
@@ -746,13 +749,13 @@
|
||||
"sieve_desc": "Breve descrição",
|
||||
"sieve_type": "Tipo de filtro",
|
||||
"skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)",
|
||||
"sogo_access": "Encaminhamento direto para o SOGoo",
|
||||
"sogo_access_info": "Depois de fazer login, o usuário é automaticamente redirecionado para o SOGo.",
|
||||
"sogo_access": "Encaminhamento direto para o SOGo",
|
||||
"sogo_access_info": "Após o login, o usuário é automaticamente redirecionado para o SOGo.",
|
||||
"sogo_visible": "O alias é visível no SoGo",
|
||||
"sogo_visible_info": "Essa opção afeta somente objetos, que podem ser exibidos no SoGo (endereços de alias compartilhados ou não compartilhados apontando para pelo menos uma mailbox local). Se estiver oculto, um alias não aparecerá como remetente selecionável no SoGo.",
|
||||
"spam_alias": "Crie ou altere endereços de alias com limite de tempo",
|
||||
"spam_filter": "Filtro de spam",
|
||||
"spam_policy": "Adicionar ou remover itens da lista branca/negra",
|
||||
"spam_policy": "Adicionar ou remover itens da lista de permissões/bloqueio",
|
||||
"spam_score": "Defina uma pontuação de spam personalizada",
|
||||
"subfolder2": "Sincronizar na subpasta no destino <br><small>(vazio = não usar subpasta</small>)",
|
||||
"syncjob": "Editar tarefa de sincronização",
|
||||
@@ -781,7 +784,9 @@
|
||||
"mta_sts_max_age_info": "Tempo em segundos que servidores de email de recepção podem armazenar esta política em cache até buscar novamente.",
|
||||
"mta_sts_mx": "Servidor MX",
|
||||
"mta_sts_mx_info": "Permite envio apenas para nomes de host de servidor de email explicitamente listados; o MTA de envio verifica se o nome do host DNS MX corresponde à lista de políticas e permite entrega apenas com certificado TLS válido (protege contra MITM).",
|
||||
"mta_sts_mx_notice": "Múltiplos servidores MX podem ser especificados (separados por vírgulas)."
|
||||
"mta_sts_mx_notice": "Múltiplos servidores MX podem ser especificados (separados por vírgulas).",
|
||||
"sender_allowed": "Permitir enviar como este alias",
|
||||
"sender_allowed_info": "Se desativado, este alias poderá apenas receber e-mails. Use a ACL de remetente para substituir essa configuração e conceder a caixas de correio específicas permissão para enviar."
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Confirme",
|
||||
@@ -970,7 +975,7 @@
|
||||
"recipient_map_new": "Novo destinatário",
|
||||
"recipient_map_new_info": "O destino do mapa do destinatário deve ser um endereço de e-mail válido ou um nome de domínio.",
|
||||
"recipient_map_old": "Destinatário original",
|
||||
"recipient_map_old_info": "O destino original do mapa de um destinatário deve ser um endereço de e-mail válido ou um nome de domínio.",
|
||||
"recipient_map_old_info": "O destino original do mapa de destinatário deve ser um endereço de e-mail válido ou um nome de domínio.",
|
||||
"recipient_maps": "Mapas de destinatários",
|
||||
"relay_all": "Retransmita todos os destinatários",
|
||||
"relay_unknown": "Retransmitir mailboxes desconhecidas",
|
||||
@@ -1061,7 +1066,7 @@
|
||||
"notified": "Notificado",
|
||||
"qhandler_success": "Solicitação enviada com sucesso para o sistema. Agora você pode fechar a janela.",
|
||||
"qid": "Respand AID",
|
||||
"qinfo": "O sistema de quarentena salvará as mensagens rejeitadas no banco de dados (o remetente <em>não</em> terá a impressão de uma mensagem entregue), bem como as mensagens, que são entregues como cópia na pasta Lixo eletrônico de uma mailbox.\n <br>“Aprenda como spam e exclua” aprenderá uma mensagem como spam por meio do Teorema de Bayes e também calculará hashes difusos para negar mensagens semelhantes no futuro.\n <br>Esteja ciente de que aprender várias mensagens pode ser demorado, dependendo do seu sistema. <br>Os elementos da lista negra são excluídos da quarentena.",
|
||||
"qinfo": "O sistema de quarentena irá salvar no banco de dados os e-mails rejeitados (o remetente <em>não</em> terá a impressão de que o e-mail foi entregue), bem como os e-mails que são entregues como cópia na pasta de spam de uma mailbox.\n<br>“Aprender como spam e excluir” irá classificar a mensagem como spam por meio do Teorema de Bayes e também calcular hashes fuzzy para bloquear mensagens semelhantes no futuro.\n<br>Esteja ciente de que o aprendizado de múltiplas mensagens pode, dependendo do seu sistema, levar tempo. <br>Elementos na lista de bloqueio são excluídos da quarentena.",
|
||||
"qitem": "Item de quarentena",
|
||||
"quarantine": "Quarentena",
|
||||
"quick_actions": "Ações",
|
||||
@@ -1237,7 +1242,12 @@
|
||||
"webauthn": "Autenticação WebAuthn",
|
||||
"waiting_usb_auth": "<i>Aguardando o dispositivo USB...</i> <br><br>Toque no botão no seu dispositivo USB agora.",
|
||||
"waiting_usb_register": "<i>Aguardando o dispositivo USB...</i> <br><br>Digite sua senha acima e confirme seu registro tocando no botão no seu dispositivo USB.",
|
||||
"yubi_otp": "Autenticação Yubico OTP"
|
||||
"yubi_otp": "Autenticação Yubico OTP",
|
||||
"force_tfa": "Forçar o cadastro de 2FA no login",
|
||||
"force_tfa_info": "O usuário será obrigado a configurar a autenticação de dois fatores antes de acessar o painel.",
|
||||
"setup_title": "Autenticação de Dois Fatores Obrigatória",
|
||||
"setup_required": "Sua conta exige autenticação de dois fatores. Configure um método de 2FA para continuar.",
|
||||
"cancel_setup": "Cancelar e sair"
|
||||
},
|
||||
"user": {
|
||||
"action": "Ação",
|
||||
@@ -1276,7 +1286,7 @@
|
||||
"delete_ays": "Confirme o processo de exclusão.",
|
||||
"direct_aliases": "Endereços de alias diretos",
|
||||
"direct_aliases_desc": "Os endereços de alias diretos são afetados pelo filtro de spam e pelas configurações da política TLS.",
|
||||
"direct_protocol_access": "Esse usuário da mailbox tem <b>acesso externo direto</b> aos seguintes protocolos e aplicativos. Essa configuração é controlada pelo administrador. As senhas de aplicativos podem ser criadas para conceder acesso a protocolos e aplicativos individuais. <br>O botão “Login no webmail” fornece login único no SoGo e está sempre disponível.",
|
||||
"direct_protocol_access": "Este usuário da mailbox possui <b>acesso direto e externo</b> aos seguintes protocolos e aplicações. Esta configuração é controlada pelo seu administrador. Senhas de aplicativo podem ser criadas para conceder acesso a protocolos e aplicações específicos.<br>O botão “Webmail” fornece login único (SSO) para o SOGo e está sempre disponível.",
|
||||
"eas_reset": "Redefinir o cache do dispositivo ActiveSync",
|
||||
"eas_reset_help": "Em muitos casos, uma redefinição do cache do dispositivo ajudará a recuperar um perfil quebrado do ActiveSync. <br><b>Atenção:</b> Todos os elementos serão baixados novamente!",
|
||||
"eas_reset_now": "Reinicie agora",
|
||||
@@ -1352,12 +1362,12 @@
|
||||
"sogo_profile_reset": "Redefinir perfil SoGo",
|
||||
"sogo_profile_reset_help": "Isso destruirá o perfil SoGo de um usuário e <b>excluirá todos os dados de contato e calendário irrecuperáveis</b>.",
|
||||
"sogo_profile_reset_now": "Redefina o perfil agora",
|
||||
"spam_aliases": "Aliases de e-mail temporários",
|
||||
"spam_aliases": "Aliases de e-mail de spam",
|
||||
"spam_score_reset": "Redefinir para o padrão do servidor",
|
||||
"spamfilter": "Filtro de spam",
|
||||
"spamfilter_behavior": "Avaliação",
|
||||
"spamfilter_bl": "Lista negra",
|
||||
"spamfilter_bl_desc": "Endereços de e-mail na lista negra para <b>sempre</b> serem classificados como spam e rejeitados. E-mails rejeitados <b>não</b> serão copiados para a quarentena. Podem ser usados curingas. Um filtro só é aplicado a aliases diretos (aliases com uma única caixa de correio de destino), excluindo aliases abrangentes e a própria mailbox.",
|
||||
"spamfilter_bl": "Lista de bloqueio",
|
||||
"spamfilter_bl_desc": "Endereços de e-mail na lista de bloqueio para <b>sempre</b> classificar como spam e rejeitados. E-mails rejeitados <b>não</b> serão copiados para a quarentena. Coringas podem ser utilizados. O filtro é aplicado apenas a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases catch-all e a própria mailbox.",
|
||||
"spamfilter_default_score": "Valores padrão",
|
||||
"spamfilter_green": "Verde: esta mensagem não é spam",
|
||||
"spamfilter_hint": "O primeiro valor descreve a “pontuação baixa de spam”, o segundo representa a “alta pontuação de spam”.",
|
||||
@@ -1368,8 +1378,8 @@
|
||||
"spamfilter_table_empty": "Não há dados para exibir",
|
||||
"spamfilter_table_remove": "remover",
|
||||
"spamfilter_table_rule": "Regra",
|
||||
"spamfilter_wl": "Lista branca",
|
||||
"spamfilter_wl_desc": "Os endereços de e-mail incluídos na lista branca são programados para <b>nunca</b> serem classificados como spam. Podem ser usados curingas. Um filtro só é aplicado a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases abrangentes e a própria mailbox.",
|
||||
"spamfilter_wl": "Lista de permissões",
|
||||
"spamfilter_wl_desc": "Endereços de e-mail na lista de permissões são configurados para <b>nunca</b> serem classificados como spam. Coringas podem ser utilizados. O filtro é aplicado apenas a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases catch-all e a própria mailbox.",
|
||||
"spamfilter_yellow": "Amarelo: esta mensagem pode ser spam, será marcada como spam e movida para sua pasta de lixo eletrônico",
|
||||
"status": "Status",
|
||||
"sync_jobs": "Trabalhos de sincronização",
|
||||
@@ -1408,7 +1418,11 @@
|
||||
"authentication": "Autenticação",
|
||||
"overview": "Visão geral",
|
||||
"protocols": "Protocolos",
|
||||
"tfa_info": "A autenticação de dois fatores ajuda a proteger sua conta. Se você habilitá-la, precisará de senhas de aplicativo para fazer login em aplicativos ou serviços que não suportam autenticação de dois fatores (por exemplo, clientes de email)."
|
||||
"tfa_info": "A autenticação de dois fatores ajuda a proteger sua conta. Se você habilitá-la, precisará de senhas de aplicativo para fazer login em aplicativos ou serviços que não suportam autenticação de dois fatores (por exemplo, clientes de email).",
|
||||
"expire_never": "Nunca expirar",
|
||||
"forever": "Para sempre",
|
||||
"pw_update_required": "Sua conta exige a alteração de senha. Defina uma nova senha para continuar.",
|
||||
"spam_aliases_info": "Um alias de spam é um endereço de e-mail temporário que pode ser usado para proteger endereços de e-mail reais. <br>Opcionalmente, pode-se definir um tempo de expiração para que o alias seja automaticamente desativado após o período definido, descartando efetivamente endereços que tenham sido abusados ou vazados."
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Não é possível excluir o usuário conectado",
|
||||
|
||||
@@ -558,7 +558,8 @@
|
||||
"mode_invalid": "Način %s ni veljaven",
|
||||
"mx_invalid": "Zapis MX %s je neveljaven",
|
||||
"version_invalid": "Različica %s je neveljavna",
|
||||
"tfa_removal_blocked": "Dvofaktorske avtentikacije ni mogoče odstraniti, ker je obvezna za vaš račun."
|
||||
"tfa_removal_blocked": "Dvofaktorske avtentikacije ni mogoče odstraniti, ker je obvezna za vaš račun.",
|
||||
"quarantine_category_invalid": "Kategorija karantene mora biti ena od: add_header, reject, all"
|
||||
},
|
||||
"debug": {
|
||||
"containers_info": "Informacije o zabojniku",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-fwdhosts" aria-selected="false" aria-controls="tab-config-fwdhosts" role="tab" data-bs-toggle="tab">{{ lang.admin.forwarding_hosts }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-f2b" aria-selected="false" aria-controls="tab-config-f2b" role="tab" data-bs-toggle="tab">{{ lang.admin.f2b_parameters }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-quarantine" aria-selected="false" aria-controls="tab-config-quarantine" role="tab" data-bs-toggle="tab">{{ lang.admin.quarantine }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-syncjobs" aria-selected="false" aria-controls="tab-config-syncjobs" role="tab" data-bs-toggle="tab">{{ lang.admin.syncjobs }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-quota" aria-selected="false" aria-controls="tab-config-quota" role="tab" data-bs-toggle="tab">{{ lang.admin.quota_notifications }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-rsettings" aria-selected="false" aria-controls="tab-config-rsettings" role="tab" data-bs-toggle="tab">{{ lang.admin.rspamd_settings_map }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-password-settings" aria-selected="false" aria-controls="tab-config-password-settings" role="tab" data-bs-toggle="tab">{{ lang.admin.password_settings }}</button></li>
|
||||
@@ -50,6 +51,7 @@
|
||||
{% include 'admin/tab-config-fwdhosts.twig' %}
|
||||
{% include 'admin/tab-config-f2b.twig' %}
|
||||
{% include 'admin/tab-config-quarantine.twig' %}
|
||||
{% include 'admin/tab-config-syncjobs.twig' %}
|
||||
{% include 'admin/tab-config-quota.twig' %}
|
||||
{% include 'admin/tab-config-rsettings.twig' %}
|
||||
{% include 'admin/tab-config-customize.twig' %}
|
||||
|
||||
21
data/web/templates/admin/tab-config-syncjobs.twig
Normal file
21
data/web/templates/admin/tab-config-syncjobs.twig
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="tab-pane fade" id="tab-config-syncjobs" role="tabpanel" aria-labelledby="tab-config-syncjobs">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex fs-5">
|
||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-config-syncjobs">
|
||||
{{ lang.admin.syncjobs }}
|
||||
</button>
|
||||
<span class="d-none d-md-block">{{ lang.admin.syncjobs }}</span>
|
||||
</div>
|
||||
<div id="collapse-tab-config-syncjobs" class="card-body collapse" data-bs-parent="#admin-content">
|
||||
<form class="form-horizontal" data-id="syncjob_settings" role="form" method="post">
|
||||
<div class="row mb-4">
|
||||
<label class="col-sm-4 control-label text-sm-end" for="syncjobs_max_parallel">{{ lang.admin.syncjobs_max_parallel|raw }}</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" class="form-control" id="syncjobs_max_parallel" name="max_parallel" value="{{ sj_data.max_parallel }}" min="1" max="50" required>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm d-block d-sm-inline btn-success" data-action="edit_selected" data-item="self" data-id="syncjob_settings" data-api-url='edit/syncjob_settings' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@ services:
|
||||
- sogo
|
||||
|
||||
dovecot-mailcow:
|
||||
image: ghcr.io/mailcow/dovecot:2.3.21.1-2
|
||||
image: ghcr.io/mailcow/dovecot:2.3.21.1-3
|
||||
depends_on:
|
||||
- mysql-mailcow
|
||||
- netfilter-mailcow
|
||||
|
||||
Reference in New Issue
Block a user