Compare commits

..

24 Commits

Author SHA1 Message Date
FreddleSpl0it
f924b7f0e3 [Web] server-side search for syncjob datatable 2026-04-15 10:32:36 +02:00
FreddleSpl0it
9610a79c3e [Web][Dovecot] Allow parallel imapsync execution 2026-04-15 10:20:15 +02:00
milkmaker
886dbcc419 Translations update from Weblate (#7190)
* [Web] Updated lang.az-az.json

[Web] Added lang.az-az.json

Co-authored-by: Nemoralis <nemoralis@duck.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>

* [Web] Updated lang.pt-br.json

Co-authored-by: André Ribas <andre@ribassu.com>

---------

Co-authored-by: Nemoralis <nemoralis@duck.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: André Ribas <andre@ribassu.com>
2026-04-11 21:25:03 +02:00
milkmaker
dc15994d40 update postscreen_access.cidr (#7177) 2026-04-02 11:50:05 +02:00
FreddleSpl0it
ec24825280 Merge pull request #7173 from mailcow/fix/escaping
[Web][Dovecot] Improve input validation and escaping
2026-03-31 09:45:23 +02:00
FreddleSpl0it
5a00b5124b [Web][Dovecot] Add parameterized queries and input validation for quarantine_category 2026-03-29 12:08:45 +02:00
FreddleSpl0it
8c039f694f Improve template URI escaping and parameter handling 2026-03-19 12:48:43 +01:00
FreddleSpl0it
95bf46c1e4 escape HTML in autodiscover logs 2026-03-19 12:44:50 +01:00
FreddleSpl0it
edde35156d escape HTML in qitem details 2026-03-19 12:44:30 +01:00
FreddleSpl0it
84e3c32f13 escape HTML in last logins 2026-03-19 12:44:00 +01:00
FreddleSpl0it
ecb848493b add missing object-level access control 2026-03-19 12:42:45 +01:00
FreddleSpl0it
8a65b9d1c6 add missing access control 2026-03-19 12:41:47 +01:00
FreddleSpl0it
ed9264fd2a [Web] Allow force_tfa for LDAP and Keycloak users 2026-03-13 14:13:25 +01:00
FreddleSpl0it
7817dda43f [ACME] Skip subdomains covered by wildcards (DNS-01 challenge only) 2026-03-13 13:08:18 +01:00
FreddleSpl0it
018e292854 Merge pull request #7134 from mailcow/fix/7112-2
[ACME] Skip autodiscover/mta-sts subdomains covered by wildcard certificates
2026-03-13 12:39:10 +01:00
FreddleSpl0it
127fb1e8f5 [ACME] Skip autodiscover/mta-sts subdomains covered by wildcard certificates 2026-03-13 12:35:22 +01:00
milkmaker
09f09cb850 [Web] Updated lang.hu-hu.json (#7130) 2026-03-12 15:01:54 +01:00
FreddleSpl0it
d4bf377a96 Merge pull request #7121 from rezzorix/fix/theme-localstorage-staging
Fix theme localStorage collision with rspamd UI
2026-03-12 07:47:03 +01:00
FreddleSpl0it
abd6fe8c79 Merge pull request #7124 from mailcow/fix/7112
[ACME] Fix wildcard certificate conflict with MAILCOW_HOSTNAME
2026-03-12 07:46:02 +01:00
FreddleSpl0it
5f8382ef44 Merge pull request #7123 from mailcow/fix/7115
[Web] Fix LDAP/Keycloak login TypeError - missing JSON decode for attributes
2026-03-12 07:45:06 +01:00
rezzorix
03eccd4e42 added/fix: use mailcow_theme in bundled dark mode JS 2026-03-12 14:11:31 +08:00
FreddleSpl0it
1da8d1c894 [ACME] Fix wildcard certificate conflict with MAILCOW_HOSTNAME 2026-03-11 09:33:16 +01:00
FreddleSpl0it
d1feebf164 [Web] Fix LDAP/Keycloak login TypeError - missing JSON decode for attributes 2026-03-11 09:18:03 +01:00
rezzorix
293b885a85 Fix theme localStorage collision with rspamd UI 2026-03-11 13:32:53 +08:00
43 changed files with 752 additions and 310 deletions

View File

@@ -253,10 +253,20 @@ while true; do
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
fi
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}"
# Skip if subdomain matches MAILCOW_HOSTNAME
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then
continue
fi
# Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN
if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then
log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain"
continue
fi
# Validate and add subdomain
if check_domain "${FULL_SUBDOMAIN}"; then
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}")
fi
done
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
@@ -273,7 +283,10 @@ while true; do
fi
# Only add mta-sts subdomain for alias domains
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "mta-sts.${alias_domain}"; then
# Skip if mta-sts subdomain is covered by a wildcard
if is_covered_by_wildcard "mta-sts.${alias_domain}"; then
log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping"
elif check_domain "mta-sts.${alias_domain}"; then
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
fi
fi
@@ -308,13 +321,31 @@ while true; do
done
fi
# Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN
MAILCOW_HOSTNAME_COVERED=0
if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then
if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then
MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-)
log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname"
MAILCOW_HOSTNAME_COVERED=1
fi
fi
# Unique domains for server certificate
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
# create certificate for server name and fqdn SANs only
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
else
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
fi
else
# create certificate for all domains, including all subdomains from other domains [*]
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
else
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
fi
fi
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
CERT_NAME=${SERVER_SAN_VALIDATED[0]}

View File

@@ -135,3 +135,32 @@ verify_challenge_path(){
return 1
fi
}
# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN
# Usage: is_covered_by_wildcard "subdomain.example.com"
# Returns: 0 if covered, 1 if not covered
# Note: Only returns 0 (covered) when DNS-01 challenge is enabled,
# as wildcards cannot be validated with HTTP-01 challenge
is_covered_by_wildcard() {
local DOMAIN=$1
# Only skip if DNS challenge is enabled (wildcards require DNS-01)
if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then
return 1
fi
# Return early if no ADDITIONAL_SAN is set
if [[ -z ${ADDITIONAL_SAN} ]]; then
return 1
fi
# Extract parent domain (e.g., mail.example.com -> example.com)
local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-)
# Check if ADDITIONAL_SAN contains a wildcard for this parent domain
if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then
return 0 # Covered by wildcard
fi
return 1 # Not covered
}

View File

@@ -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 \

View File

@@ -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);

View File

@@ -47,7 +47,7 @@ try:
if max_score == "":
max_score = 9999.0
def query_mysql(query, headers = True, update = False):
def query_mysql(query, params = None, headers = True, update = False):
while True:
try:
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
@@ -57,7 +57,10 @@ try:
else:
break
cur = cnx.cursor()
cur.execute(query)
if params:
cur.execute(query, params)
else:
cur.execute(query)
if not update:
result = []
columns = tuple( [d[0] for d in cur.description] )
@@ -76,7 +79,7 @@ try:
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
if category == "add_header": category = "add header"
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category))
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
if len(meta_query) == 0:
return
@@ -130,7 +133,7 @@ try:
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
server.quit()
for res in meta_query:
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True)
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
break
except Exception as ex:
@@ -138,7 +141,7 @@ try:
print('%s' % (ex))
time.sleep(3)
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score))
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,))
for record in records:
attrs = ''
@@ -156,7 +159,7 @@ try:
except Exception as ex:
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
last_notification = 0
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],))
attrs = attrs_json[0]['attributes']
if isinstance(attrs, str):
# if attr is str then just load it

View File

@@ -66,7 +66,7 @@ $_SESSION['acl']['tls_policy'] = "1";
$_SESSION['acl']['quarantine_notification'] = "1";
$_SESSION['acl']['quarantine_category'] = "1";
$_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_redirection'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
$_SESSION['acl']['unlimited_quota'] = "1";

View File

@@ -66,7 +66,7 @@ $_SESSION['acl']['tls_policy'] = "1";
$_SESSION['acl']['quarantine_notification'] = "1";
$_SESSION['acl']['quarantine_category'] = "1";
$_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_redirection'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
$_SESSION['acl']['unlimited_quota'] = "1";

View File

@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Sun Mar 1 00:29:01 UTC 2026
# Whitelist generated by Postwhite v3.4 on Wed Apr 1 00:33:31 UTC 2026
# https://github.com/stevejenkins/postwhite/
# 2174 total rules
# 2246 total rules
2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit
2a01:111:f403:2800::/53 permit
@@ -31,8 +31,10 @@
2a02:a60:0:5::/64 permit
2a0f:f640::/56 permit
2c0f:fb50:4000::/36 permit
2.207.151.32/27 permit
2.207.151.53 permit
2.207.217.30 permit
2.207.223.160/27 permit
3.64.237.68 permit
3.65.3.180 permit
3.70.123.177 permit
@@ -58,8 +60,8 @@
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit
13.107.213.51 permit
13.107.246.51 permit
13.107.213.38 permit
13.107.246.38 permit
13.108.16.0/20 permit
13.110.208.0/21 permit
13.110.209.0/24 permit
@@ -368,6 +370,7 @@
64.132.88.0/23 permit
64.132.92.0/24 permit
64.181.194.190 permit
64.181.213.254 permit
64.207.219.7 permit
64.207.219.8 permit
64.207.219.9 permit
@@ -453,7 +456,11 @@
66.218.75.252/31 permit
66.218.75.254 permit
66.220.144.128/25 permit
66.220.144.178 permit
66.220.144.179 permit
66.220.155.0/24 permit
66.220.155.178 permit
66.220.155.179 permit
66.220.157.0/25 permit
66.231.80.0/20 permit
66.240.227.0/24 permit
@@ -518,6 +525,8 @@
69.162.98.0/24 permit
69.169.224.0/20 permit
69.171.232.0/24 permit
69.171.232.180 permit
69.171.232.181 permit
69.171.244.0/23 permit
70.42.149.35 permit
72.3.185.0/24 permit
@@ -639,6 +648,7 @@
77.238.189.148/30 permit
79.135.106.0/24 permit
79.135.107.0/24 permit
80.225.160.128/25 permit
81.169.146.243 permit
81.169.146.245 permit
81.169.146.246 permit
@@ -657,6 +667,9 @@
82.165.159.45 permit
82.165.159.130 permit
82.165.159.131 permit
84.8.68.0/25 permit
84.8.192.128/25 permit
84.8.224.128/25 permit
85.9.206.169 permit
85.9.210.45 permit
85.158.136.0/21 permit
@@ -1408,6 +1421,20 @@
129.146.88.28 permit
129.146.147.105 permit
129.146.236.58 permit
129.148.135.0/25 permit
129.148.148.0/25 permit
129.148.164.0/25 permit
129.148.180.0/25 permit
129.148.215.0/25 permit
129.149.6.0/25 permit
129.149.22.0/25 permit
129.149.38.0/25 permit
129.149.52.0/25 permit
129.149.68.0/25 permit
129.149.84.0/25 permit
129.149.100.0/25 permit
129.149.118.0/25 permit
129.149.126.0/25 permit
129.151.67.221 permit
129.153.62.216 permit
129.153.104.71 permit
@@ -1420,16 +1447,19 @@
129.159.22.159 permit
129.159.87.137 permit
129.213.195.191 permit
130.35.116.0/25 permit
130.61.9.72 permit
130.162.39.83 permit
130.248.172.0/24 permit
130.248.173.0/24 permit
131.186.12.0/25 permit
131.253.30.0/24 permit
131.253.121.0/26 permit
132.145.13.209 permit
132.226.26.225 permit
132.226.49.32 permit
132.226.56.24 permit
134.98.248.128/25 permit
134.128.64.0/19 permit
134.128.96.0/19 permit
134.170.27.8 permit
@@ -1460,6 +1490,10 @@
136.147.182.0/24 permit
136.147.224.0/20 permit
136.179.50.206 permit
136.248.224.128/25 permit
136.248.232.128/25 permit
138.1.108.0/25 permit
138.1.170.0/24 permit
139.60.152.0/22 permit
139.138.35.44 permit
139.138.46.121 permit
@@ -1468,6 +1502,7 @@
139.138.57.55 permit
139.138.58.119 permit
139.167.79.86 permit
139.177.108.0/25 permit
139.180.17.0/24 permit
140.238.148.191 permit
141.148.55.217 permit
@@ -1506,6 +1541,9 @@
146.88.28.0/24 permit
146.148.116.76 permit
147.154.32.0/25 permit
147.154.63.0/24 permit
147.154.126.0/24 permit
147.154.191.0/24 permit
147.243.1.47 permit
147.243.1.48 permit
147.243.1.153 permit
@@ -1517,12 +1555,16 @@
149.72.234.184 permit
149.72.248.236 permit
149.97.173.180 permit
149.118.160.128/25 permit
150.136.21.199 permit
150.230.98.160 permit
151.145.38.14 permit
152.67.105.195 permit
152.69.200.236 permit
152.70.155.126 permit
155.248.135.128/25 permit
155.248.140.0/25 permit
155.248.148.0/25 permit
155.248.208.51 permit
155.248.220.138 permit
155.248.234.149 permit
@@ -1531,10 +1573,14 @@
157.58.30.128/25 permit
157.58.196.96/29 permit
157.58.249.3 permit
157.137.32.128/25 permit
157.137.96.128/25 permit
157.151.208.65 permit
157.255.1.64/29 permit
158.101.211.207 permit
158.247.16.0/20 permit
158.247.100.0/25 permit
159.13.4.0/25 permit
159.92.154.0/24 permit
159.92.155.0/24 permit
159.92.157.0/24 permit
@@ -1568,6 +1614,7 @@
161.71.64.0/20 permit
162.88.4.0/23 permit
162.88.8.0/24 permit
162.88.24.0/23 permit
162.88.24.0/24 permit
162.88.25.0/24 permit
162.88.36.0/24 permit
@@ -1585,10 +1632,12 @@
164.152.23.32 permit
164.152.25.241 permit
164.177.132.168/30 permit
165.1.100.0/25 permit
165.173.128.0/24 permit
165.173.180.1 permit
165.173.180.250/31 permit
165.173.182.250/31 permit
165.173.189.205 permit
166.78.68.0/22 permit
166.78.68.221 permit
166.78.69.169 permit
@@ -1607,9 +1656,12 @@
167.89.75.126 permit
167.89.75.136 permit
167.89.75.164 permit
167.89.101.2 permit
167.89.101.192/28 permit
167.220.67.232/29 permit
168.107.248.128/25 permit
168.110.160.128/25 permit
168.110.248.128/25 permit
168.129.184.128/25 permit
168.129.248.128/25 permit
168.138.5.36 permit
168.138.73.51 permit
168.138.77.31 permit
@@ -1745,8 +1797,24 @@
192.18.139.154 permit
192.18.145.36 permit
192.18.152.58 permit
192.22.32.128/25 permit
192.22.96.128/25 permit
192.22.160.128/25 permit
192.22.224.128/25 permit
192.28.128.0/18 permit
192.29.24.0/25 permit
192.29.44.0/25 permit
192.29.72.0/25 permit
192.29.88.0/25 permit
192.29.103.128/25 permit
192.29.134.0/25 permit
192.29.151.128/25 permit
192.29.172.0/25 permit
192.29.178.0/25 permit
192.29.200.0/25 permit
192.29.216.0/25 permit
192.29.232.0/25 permit
192.29.248.0/25 permit
192.30.252.0/22 permit
192.161.144.0/20 permit
192.162.87.0/24 permit
@@ -1754,11 +1822,6 @@
192.237.159.42 permit
192.237.159.43 permit
192.254.112.0/20 permit
192.254.112.60 permit
192.254.112.98/31 permit
192.254.113.10 permit
192.254.113.101 permit
192.254.114.176 permit
193.109.254.0/23 permit
193.122.128.100 permit
193.123.56.63 permit
@@ -1927,6 +1990,7 @@
207.211.30.128/25 permit
207.211.31.0/25 permit
207.211.41.113 permit
207.211.132.0/25 permit
207.218.90.0/24 permit
207.218.90.122 permit
207.250.68.0/24 permit
@@ -1934,6 +1998,8 @@
208.43.21.28/30 permit
208.43.21.64/29 permit
208.43.21.72/30 permit
208.56.9.224 permit
208.56.13.196 permit
208.64.132.0/22 permit
208.71.40.63 permit
208.71.40.64/31 permit
@@ -1960,6 +2026,7 @@
208.71.42.214 permit
208.72.249.240/29 permit
208.75.120.0/22 permit
208.76.62.0/23 permit
208.76.62.0/24 permit
208.76.63.0/24 permit
208.82.237.96/29 permit
@@ -2120,6 +2187,7 @@
216.136.168.80/28 permit
216.139.64.0/19 permit
216.145.221.0/24 permit
216.146.32.0/23 permit
216.146.32.0/24 permit
216.146.33.0/24 permit
216.198.0.0/18 permit
@@ -2140,6 +2208,7 @@
223.165.120.0/23 permit
2001:0868:0100:0600::/64 permit
2001:4860:4000::/36 permit
2001:4860:4864::/56 permit
2001:748:100:40::2:0/112 permit
2001:748:400:1300::3 permit
2001:748:400:1300::4 permit
@@ -2157,10 +2226,12 @@
2001:748:400:3301::3 permit
2001:748:400:3301::4 permit
2404:6800:4000::/36 permit
2404:6800:4864::/56 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
2607:f8b0:4000::/36 permit
2607:f8b0:4864::/56 permit
2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit
2620:109:c006:104::/64 permit
@@ -2172,8 +2243,9 @@
2620:10d:c09c:400::8:1 permit
2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit
2620:1ec:46::51 permit
2620:1ec:bdf::51 permit
2620:1ec:46::38 permit
2620:1ec:bdf::38 permit
2800:3f0:4000::/36 permit
2800:3f0:4864::/56 permit
49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email

View 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'),

View File

@@ -754,7 +754,7 @@ paths:
- syncjobs
- quarantine
- login_as
- sogo_redirection
- sogo_access
- app_passwds
- bcc_maps
- pushover
@@ -807,7 +807,7 @@ paths:
- syncjobs
- quarantine
- login_as
- sogo_redirection
- sogo_access
- app_passwds
- bcc_maps
- pushover
@@ -3423,7 +3423,7 @@ paths:
- info@domain2.tld
- domain3.tld
- "*"
sogo_redirection: "1"
sogo_access: "1"
username:
- info@domain.tld
tags: ["tag3", "tag4"]
@@ -3474,7 +3474,7 @@ paths:
- info@domain2.tld
- domain3.tld
- "*"
sogo_redirection: "1"
sogo_access: "1"
tags: ["tag3", "tag4"]
items:
- info@domain.tld
@@ -3506,7 +3506,7 @@ paths:
sender_acl:
description: list of allowed send from addresses
type: object
sogo_redirection:
sogo_access:
description: is access to SOGo webmail active or not
type: boolean
type: object
@@ -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:
@@ -4883,7 +4941,7 @@ paths:
force_pw_update: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_redirection: "1"
sogo_access: "1"
tls_enforce_in: "0"
tls_enforce_out: "0"
domain: doman3.tld
@@ -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:
@@ -5807,7 +5895,7 @@ paths:
force_pw_update: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_redirection: "1"
sogo_access: "1"
tls_enforce_in: "0"
tls_enforce_out: "0"
custom_attributes: {}

View File

@@ -48,12 +48,7 @@ function acl($_action, $_scope = null, $_data = null, $_extra = null) {
);
continue;
}
$sogo_acl_changed = false;
foreach ($set_acls as $set_acl_key => $set_acl_val) {
// Track if sogo_access ACL changed
if ($set_acl_key == 'sogo_access' && $is_now[$set_acl_key] != $set_acl_val) {
$sogo_acl_changed = true;
}
$stmt = $pdo->prepare("UPDATE `user_acl` SET " . $set_acl_key . " = " . intval($set_acl_val) . "
WHERE `username` = :username");
$stmt->execute(array(
@@ -65,10 +60,6 @@ function acl($_action, $_scope = null, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('acl_saved', $username)
);
// Update SOGo static view if sogo_access ACL changed
if ($sogo_acl_changed) {
update_sogo_static_view($username);
}
}
break;
case 'domainadmin':

View File

@@ -287,6 +287,8 @@ function user_login($user, $pass, $extra = null){
return false;
}
$row['attributes'] = json_decode($row['attributes'], true);
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
@@ -343,6 +345,8 @@ function user_login($user, $pass, $extra = null){
return false;
}
$row['attributes'] = json_decode($row['attributes'], true);
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {

View File

@@ -108,6 +108,14 @@ function fwdhost($_action, $_data = null) {
}
break;
case 'delete':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$hosts = (array)$_data['forwardinghost'];
foreach ($hosts as $host) {
try {

View File

@@ -940,12 +940,8 @@ function update_sogo_static_view($mailbox = null) {
$mailbox_exists = false;
if ($mailbox !== null) {
// Check if the mailbox exists and should have SOGo access
$stmt = $pdo->prepare("SELECT m.username FROM mailbox m
LEFT JOIN user_acl u ON m.username = u.username
WHERE m.username = :mailbox
AND m.active = '1'
AND (u.sogo_access IS NULL OR u.sogo_access = 1)");
// Check if the mailbox exists
$stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'");
$stmt->execute(array(':mailbox' => $mailbox));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row){
@@ -980,10 +976,8 @@ function update_sogo_static_view($mailbox = null) {
LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
LEFT OUTER JOIN user_acl ON user_acl.username = mailbox.username
WHERE
mailbox.active = '1'
AND (user_acl.sogo_access IS NULL OR user_acl.sogo_access = 1)
$subquery
ON DUPLICATE KEY UPDATE
`domain` = VALUES(`domain`),
@@ -1011,27 +1005,7 @@ function update_sogo_static_view($mailbox = null) {
));
}
if ($mailbox_exists) {
// For single mailbox update, only delete this specific user
$stmt = $pdo->prepare("DELETE FROM _sogo_static_view
WHERE `c_uid` = :mailbox
AND `c_uid` NOT IN (
SELECT m.`username` FROM `mailbox` m
LEFT JOIN `user_acl` u ON m.`username` = u.`username`
WHERE m.`active` = '1'
AND m.`username` = :mailbox2
AND (u.`sogo_access` IS NULL OR u.`sogo_access` = 1)
)");
$stmt->execute(array(':mailbox' => $mailbox, ':mailbox2' => $mailbox));
} else {
// Full cleanup for all users
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (
SELECT m.`username` FROM `mailbox` m
LEFT JOIN `user_acl` u ON m.`username` = u.`username`
WHERE m.`active` = '1'
AND (u.`sogo_access` IS NULL OR u.`sogo_access` = 1)
);");
}
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
flush_memcached();
}
@@ -3516,14 +3490,9 @@ function set_user_loggedin_session($user) {
session_regenerate_id(true);
$_SESSION['mailcow_cc_username'] = $user;
$_SESSION['mailcow_cc_role'] = 'user';
acl('to_session');
if (hasACLAccess("sogo_access")) {
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
$_SESSION['sogo-sso-user-allowed'][] = $user;
$_SESSION['sogo-sso-pass'] = $sogo_sso_pass;
}
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
$_SESSION['sogo-sso-user-allowed'][] = $user;
$_SESSION['sogo-sso-pass'] = $sogo_sso_pass;
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);

View File

@@ -1101,7 +1101,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
$sogo_redirection = (isset($_data['sogo_redirection'])) ? intval($_data['sogo_redirection']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_redirection']);
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$imap_access = (isset($_data['imap_access'])) ? intval($_data['imap_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
@@ -1111,10 +1111,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
// Validate quarantine_category
if (!in_array($quarantine_category, array('add_header', 'reject', 'all'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'quarantine_category_invalid'
);
return false;
}
$quota_b = ($quota_m * 1048576);
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
$force_pw_update = 0;
}
if ($authsource == 'generic-oidc'){
$force_tfa = 0;
}
$mailbox_attrs = json_encode(
@@ -1123,7 +1134,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'force_tfa' => strval($force_tfa),
'tls_enforce_in' => strval($tls_enforce_in),
'tls_enforce_out' => strval($tls_enforce_out),
'sogo_redirection' => strval($sogo_redirection),
'sogo_access' => strval($sogo_access),
'imap_access' => strval($imap_access),
'pop3_access' => strval($pop3_access),
'smtp_access' => strval($smtp_access),
@@ -1314,7 +1325,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
$_data['eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
$_data['sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
$_data['sogo_access'] = (in_array('sogo_access', $_data['acl'])) ? 1 : 0;
$_data['pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
$_data['quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
$_data['quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
@@ -1331,7 +1341,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['syncjobs'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_syncjobs']);
$_data['eas_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_eas_reset']);
$_data['sogo_profile_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_profile_reset']);
$_data['sogo_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_access']);
$_data['pushover'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pushover']);
$_data['quarantine'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine']);
$_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
@@ -1343,9 +1352,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
try {
$stmt = $pdo->prepare("INSERT INTO `user_acl`
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, `sogo_access`,
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`)
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, :sogo_access,
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) ");
$stmt->execute(array(
':username' => $username,
@@ -1357,7 +1366,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':syncjobs' => $_data['syncjobs'],
':eas_reset' => $_data['eas_reset'],
':sogo_profile_reset' => $_data['sogo_profile_reset'],
':sogo_access' => $_data['sogo_access'],
':pushover' => $_data['pushover'],
':quarantine' => $_data['quarantine'],
':quarantine_attachments' => $_data['quarantine_attachments'],
@@ -1734,11 +1742,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
// Validate quarantine_category
if (!in_array($attr["quarantine_category"], array('add_header', 'reject', 'all'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
'msg' => 'quarantine_category_invalid'
);
return false;
}
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
$attr["sogo_redirection"] = isset($_data['sogo_redirection']) ? intval($_data['sogo_redirection']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_redirection']);
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
@@ -1769,7 +1786,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
$attr['acl_eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_access'] = (in_array('sogo_access', $_data['acl'])) ? 1 : 0;
$attr['acl_pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
@@ -1787,7 +1803,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_syncjobs'] = 0;
$attr['acl_eas_reset'] = 0;
$attr['acl_sogo_profile_reset'] = 0;
$attr['acl_sogo_access'] = 0;
$attr['acl_pushover'] = 0;
$attr['acl_quarantine'] = 0;
$attr['acl_quarantine_attachments'] = 0;
@@ -2065,6 +2080,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false;
}
foreach ($usernames as $username) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if ($_data['spam_score'] == "default") {
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
@@ -2242,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();
@@ -3108,7 +3160,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($is_now['attributes']['force_tfa']);
(int)$sogo_redirection = (isset($_data['sogo_redirection'])) ? intval($_data['sogo_redirection']) : intval($is_now['attributes']['sogo_redirection']);
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
@@ -3131,6 +3183,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
$force_pw_update = 0;
}
if ($authsource == 'generic-oidc'){
$force_tfa = 0;
}
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
@@ -3404,7 +3458,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`authsource` = :authsource,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.force_tfa', :force_tfa),
`attributes` = JSON_SET(`attributes`, '$.sogo_redirection', :sogo_redirection),
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
@@ -3422,7 +3476,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':attribute_hash' => $attribute_hash,
':force_pw_update' => $force_pw_update,
':force_tfa' => $force_tfa,
':sogo_redirection' => $sogo_redirection,
':sogo_access' => $sogo_access,
':imap_access' => $imap_access,
':pop3_access' => $pop3_access,
':sieve_access' => $sieve_access,
@@ -3791,10 +3845,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
// Validate quarantine_category
if (!in_array($attr["quarantine_category"], array('add_header', 'reject', 'all'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
'msg' => 'quarantine_category_invalid'
);
continue;
}
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
$attr["sogo_redirection"] = isset($_data['sogo_redirection']) ? intval($_data['sogo_redirection']) : $is_now['sogo_redirection'];
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out'];
@@ -3822,7 +3885,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
$attr['acl_eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
$attr['acl_sogo_access'] = (in_array('sogo_access', $_data['acl'])) ? 1 : 0;
$attr['acl_pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
@@ -4586,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)) {

View File

@@ -89,7 +89,7 @@ $globalVariables = [
'app_links' => $app_links,
'app_links_processed' => $app_links_processed,
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
'uri' => $_SERVER['REQUEST_URI'],
'uri' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/',
];
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {

View File

@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "12032026_1300";
$db_version = "19022026_1220";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -506,7 +506,6 @@ function init_db_schema()
"syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'",
"eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'",
"sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"pushover" => "TINYINT(1) NOT NULL DEFAULT '1'",
// quarantine is for quarantine actions, todo: rename
"quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
@@ -706,7 +705,7 @@ function init_db_schema()
"syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
"quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
"login_as" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_redirection" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
"app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'",
"bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'",
"pushover" => "TINYINT(1) NOT NULL DEFAULT '0'",
@@ -1398,10 +1397,7 @@ function init_db_schema()
$pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;");
$pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;");
// Migrate sogo_access attribute to sogo_redirection
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_redirection', JSON_VALUE(`attributes`, '$.sogo_access')) WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_REMOVE(`attributes`, '$.sogo_access') WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_redirection', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_redirection') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;");
@@ -1425,10 +1421,6 @@ function init_db_schema()
// Fix domain_admins
$pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';");
// Migrate template sogo_access to sogo_redirection
$pdo->query("UPDATE `templates` SET `attributes` = JSON_SET(`attributes`, '$.sogo_redirection', JSON_VALUE(`attributes`, '$.sogo_access')) WHERE `type` = 'mailbox' AND JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;");
$pdo->query("UPDATE `templates` SET `attributes` = JSON_REMOVE(`attributes`, '$.sogo_access') WHERE `type` = 'mailbox' AND JSON_VALUE(`attributes`, '$.sogo_access') IS NOT NULL;");
// add default templates
$default_domain_template = array(
"template" => "Default",
@@ -1464,7 +1456,7 @@ function init_db_schema()
"rl_value" => "",
"force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']),
"force_tfa" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_tfa']),
"sogo_redirection" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_redirection']),
"sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']),
"active" => 1,
"tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
"tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']),
@@ -1480,7 +1472,6 @@ function init_db_schema()
"acl_syncjobs" => 0,
"acl_eas_reset" => 1,
"acl_sogo_profile_reset" => 0,
"acl_sogo_access" => 1,
"acl_pushover" => 1,
"acl_quarantine" => 1,
"acl_quarantine_attachments" => 1,

View File

@@ -81,9 +81,8 @@ if (isset($_POST["verify_tfa_login"])) {
header("Location: /");
die();
}
if (intval($user_details['attributes']['sogo_redirection']) == 1 &&
if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
hasACLAccess('sogo_access') &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/");
@@ -162,9 +161,8 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
header("Location: /");
die();
}
if (intval($user_details['attributes']['sogo_redirection']) == 1 &&
if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
hasACLAccess('sogo_access') &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/");

View File

@@ -13,7 +13,9 @@ $twig = new Environment($loader, [
// functions
$twig->addFunction(new TwigFunction('query_string', function (array $params = []) {
return http_build_query(array_merge($_GET, $params));
$allowed = ['lang', 'mobileconfig'];
$filtered = array_intersect_key($_GET, array_flip($allowed));
return http_build_query(array_merge($filtered, $params));
}));
$twig->addFunction(new TwigFunction('is_uri', function (string $uri, string $where = null) {

View File

@@ -196,8 +196,8 @@ $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
// Force 2FA enrollment at next login
$MAILBOX_DEFAULT_ATTRIBUTES['force_tfa'] = false;
// Enable SOGo redirection - Users will be redirected to SOGo after login (set to false to disable redirect by default)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_redirection'] = true;
// Enable SOGo access - Users will be redirected to SOGo after login (set to false to disable redirect by default)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
// How to handle tagged emails
// none - No special handling

View File

@@ -12,9 +12,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_redirection']) == 1 &&
hasACLAccess('sogo_access') &&
!$is_dual && getenv('SKIP_SOGO') != "y") {
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") {
header("Location: /SOGo/so/");
} else {
header("Location: /user");

View File

@@ -345,7 +345,7 @@ $(document).ready(function() {
$('.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('theme', 'light');
localStorage.setItem('mailcow_theme', 'light');
}else{
$('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
$('#dark-mode-toggle').prop('checked', true);
@@ -353,7 +353,7 @@ $(document).ready(function() {
$('.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('theme', 'dark');
localStorage.setItem('mailcow_theme', 'dark');
}
}

View File

@@ -1128,6 +1128,11 @@ jQuery(function($){
item.ua = escapeHtml(item.ua);
}
item.ua = '<span style="font-size:small">' + item.ua + '</span>';
if (item.user == null) {
item.user = 'unknown';
} else {
item.user = escapeHtml(item.user);
}
if (item.service == "activesync") {
item.service = '<span class="badge fs-6 bg-info">ActiveSync</span>';
}

View File

@@ -1,5 +1,6 @@
$(document).ready(function() {
var theme = localStorage.getItem("theme");
localStorage.clear();
localStorage.setItem("theme", theme);
var theme = localStorage.getItem("mailcow_theme");
if (theme !== null) {
localStorage.setItem("mailcow_theme", theme);
}
});

View File

@@ -385,9 +385,6 @@ $(document).ready(function() {
if (template.acl_sogo_profile_reset == 1){
acl.push("sogo_profile_reset");
}
if (template.acl_sogo_access == 1){
acl.push("sogo_access");
}
if (template.acl_pushover == 1){
acl.push("pushover");
}
@@ -427,10 +424,10 @@ $(document).ready(function() {
} else {
$('#force_pw_update').prop('checked', false);
}
if (template.sogo_redirection == 1){
$('#sogo_redirection').prop('checked', true);
if (template.sogo_access == 1){
$('#sogo_access').prop('checked', true);
} else {
$('#sogo_redirection').prop('checked', false);
$('#sogo_access').prop('checked', false);
}
// load tags
@@ -1244,7 +1241,7 @@ jQuery(function($){
item.attributes.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sieve_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.eas_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.dav_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_redirection = '<i class="text-' + (item.attributes.sogo_redirection == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_redirection == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_redirection == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>';
if (item.attributes.quarantine_notification === 'never') {
item.attributes.quarantine_notification = lang.never;
} else if (item.attributes.quarantine_notification === 'hourly') {
@@ -1363,8 +1360,8 @@ jQuery(function($){
defaultContent: '',
},
{
title: 'SOGO redirection',
data: 'attributes.sogo_redirection',
title: 'SOGO',
data: 'attributes.sogo_access',
defaultContent: '',
},
{
@@ -2153,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>>" +
@@ -2165,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) {
@@ -2204,7 +2206,7 @@ jQuery(function($){
item.exit_status = item.success + ' ' + item.exit_status;
});
return json;
return json.data;
}
},
columns: [
@@ -2228,6 +2230,7 @@ jQuery(function($){
{
title: 'ID',
data: 'id',
searchable: false,
responsivePriority: 3,
defaultContent: ''
},
@@ -2245,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>';
@@ -2268,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: ''

View File

@@ -226,18 +226,18 @@ jQuery(function($){
}
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) {
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + escapeHtml(value) + '</p>');
});
} else {
$('#qid_detail_fuzzy').append('-');
}
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action == "add header") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + escapeHtml(data.score) + '</b> - ' + lang.junk_folder + '</span>');
} else if (data.action == "reject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + escapeHtml(data.score) + '</b> - ' + lang.rejected + '</span>');
} else if (data.action == "rewrite subject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + escapeHtml(data.score) + '</b> - ' + lang.rewrite_subject + '</span>');
}
}
if (typeof data.recipients !== 'undefined') {
@@ -254,8 +254,8 @@ jQuery(function($){
qAtts.text('');
$.each(data.attachments, function(index, value) {
qAtts.append(
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
'<p><a href="/inc/ajax/qitem_details.php?id=' + escapeHtml(qitem) + '&amp;att=' + index + '" target="_blank">' + escapeHtml(value[0]) + '</a> (' + escapeHtml(value[1]) + ')' +
' - <small><a href="' + escapeHtml(value[3]) + '" target="_blank">' + lang.check_hash + '</a></small></p>'
);
});
}

View File

@@ -98,8 +98,8 @@ jQuery(function($){
var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
var service = '<div class="badge bg-secondary">' + item.service.toUpperCase() + '</div>';
var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-key-fill"></i><span class="ms-1">' + escapeHtml(item.app_password_name || "App") + '</span></a>' : '';
var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.tools/prefix/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
var real_rip = item.real_rip.startsWith("Web") ? escapeHtml(item.real_rip) : '<a href="https://bgp.tools/prefix/' + escapeHtml(item.real_rip) + '" target="_blank">' + escapeHtml(item.real_rip) + "</a>";
var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + escapeHtml(item.location.toLowerCase()) + '"></span>' : '';
var ip_data = real_rip + ip_location + app_password;
$(".last-sasl-login").append(`

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
{
"acl": {
"login_as": "E-poçt qutusu istifadəçisi olaraq daxil ol"
}
}

View File

@@ -22,15 +22,14 @@
"ratelimit": "Rate limit",
"recipient_maps": "Empfängerumschreibungen",
"smtp_ip_access": "Verwalten der erlaubten Hosts für SMTP",
"sogo_access": "SOGo-Nutzung erlauben",
"sogo_access": "Verwalten des SOGo-Zugriffsrechts erlauben",
"sogo_profile_reset": "SOGo-Profil zurücksetzen",
"spam_alias": "Temporäre E-Mail-Aliasse",
"spam_policy": "Deny/Allowlist",
"spam_score": "Spam-Bewertung",
"syncjobs": "Sync Jobs",
"tls_policy": "Verschlüsselungsrichtlinie",
"unlimited_quota": "Unendliche Quota für Mailboxen",
"sogo_redirection": "Verwalten der SOGo-Weiterleitung erlauben"
"unlimited_quota": "Unendliche Quota für Mailboxen"
},
"add": {
"activate_filter_warn": "Alle anderen Filter dieses Typs werden deaktiviert, falls dieses Script aktiviert wird.",
@@ -373,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",
@@ -513,6 +514,7 @@
"pushover_credentials_missing": "Pushover Token und/oder Key fehlen",
"pushover_key": "Pushover Key hat das falsche Format",
"pushover_token": "Pushover Token hat das falsche Format",
"quarantine_category_invalid": "Quarantäne-Kategorie muss eine der folgenden sein: add_header, reject, all",
"quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein",
"recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits",
"recovery_email_failed": "E-Mail zur Wiederherstellung konnte nicht gesendet werden. Bitte wenden Sie sich an Ihren Administrator.",
@@ -768,6 +770,8 @@
"sieve_desc": "Kurze Beschreibung",
"sieve_type": "Filtertyp",
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
"sogo_access": "Direktes weiterleiten an SOGo",
"sogo_access_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.",
"sogo_visible": "Alias in SOGo sichtbar",
"sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
"spam_alias": "Anpassen temporärer Alias-Adressen",
@@ -784,9 +788,7 @@
"unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername",
"validate_save": "Validieren und speichern",
"pushover_sound": "Ton",
"sogo_redirection": "Direktes weiterleiten an SOGo",
"sogo_redirection_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet."
"pushover_sound": "Ton"
},
"fido2": {
"confirm": "Bestätigen",
@@ -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",

View File

@@ -22,15 +22,14 @@
"ratelimit": "Rate limit",
"recipient_maps": "Recipient maps",
"smtp_ip_access": "Change allowed hosts for SMTP",
"sogo_access": "Allow SOGo usage",
"sogo_access": "Allow management of SOGo access",
"sogo_profile_reset": "Reset SOGo profile",
"spam_alias": "Temporary aliases",
"spam_policy": "Denylist/Allowlist",
"spam_score": "Spam score",
"syncjobs": "Sync jobs",
"tls_policy": "TLS policy",
"unlimited_quota": "Unlimited quota for mailboxes",
"sogo_redirection": "Allow management of SOGo forwarding"
"unlimited_quota": "Unlimited quota for mailboxes"
},
"add": {
"activate_filter_warn": "All other filters will be deactivated, when active is checked.",
@@ -383,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",
@@ -514,6 +515,7 @@
"pushover_credentials_missing": "Pushover token and or key missing",
"pushover_key": "Pushover key has a wrong format",
"pushover_token": "Pushover token has a wrong format",
"quarantine_category_invalid": "Quarantine category must be one of: add_header, reject, all",
"quota_not_0_not_numeric": "Quota must be numeric and >= 0",
"recipient_map_entry_exists": "A Recipient map entry \"%s\" exists",
"recovery_email_failed": "Could not send a recovery email. Please contact your administrator.",
@@ -769,6 +771,8 @@
"sieve_desc": "Short description",
"sieve_type": "Filter type",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"sogo_access": "Direct forwarding to SOGo",
"sogo_access_info": "After logging in, the user is automatically redirected to SOGo.",
"sogo_visible": "Alias is visible in SOGo",
"sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
"spam_alias": "Create or change time limited alias addresses",
@@ -784,9 +788,7 @@
"title": "Edit object",
"unchanged_if_empty": "If unchanged leave blank",
"username": "Username",
"validate_save": "Validate and save",
"sogo_redirection": "Direct forwarding to SOGo",
"sogo_redirection_info": "After logging in, the user is automatically redirected to SOGo."
"validate_save": "Validate and save"
},
"fido2": {
"confirm": "Confirm",
@@ -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",

View File

@@ -1144,7 +1144,8 @@
"subscribeall": "Feliratkozás minden mappára",
"syncjob": "Szinkronizálási feladat hozzáadása",
"internal": "Belső",
"internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető."
"internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető.",
"sender_allowed": "Küldés engedélyezése ezzel az aliasszal"
},
"danger": {
"access_denied": "Hozzáférés megtagatva vagy nem megfelelő űrlap adat",
@@ -1245,6 +1246,21 @@
"pushover_key": "A pushover kulcs rossz formátumú",
"pushover_token": "A Pushover token rossz formátumú",
"quota_not_0_not_numeric": "A kvótának numerikusnak és >= 0-nak kell lennie.",
"recipient_map_entry_exists": "Létezik egy \"%s\" címzett-térkép bejegyzés"
"recipient_map_entry_exists": "Létezik egy \"%s\" címzett-térkép bejegyzés",
"redis_error": "Redis hiba lépett fel: %s",
"relayhost_invalid": "A(z) %s elem érvénytelen a leképezésben.",
"release_send_failed": "Az üzenet felszabadítása sikertelen: %s",
"reset_f2b_regex": "A regex-szűrő időtúllépés miatt nem állt le. Próbálja újra, vagy várjon egy kicsit, és töltse újra az oldalt.",
"resource_invalid": "A(z) %s erőforrásnév érvénytelen",
"rl_timeframe": "Érvénytelen időkeret a lekérdezési korláthoz",
"rspamd_ui_pw_length": "A Rspamd UI jelszónak legalább 6 karakter hosszúnak kell lennie.",
"script_empty": "A szkript nem lehet üres",
"sender_acl_invalid": "A küldőhöz tartozó ACL-érték (%s) érvénytelen",
"set_acl_failed": "Az ACL beállítása meghiúsult",
"settings_map_invalid": "Érvénytelen beállítás-leképezési azonosító: %s",
"recovery_email_failed": "A helyreállítási email kiküldése sikertelen. Kérlek, lépj kapcsolatba az adminisztrátorral!",
"reset_token_limit_exceeded": "Túl sok visszaállítási kísérlet. Kérjük, várjon, mielőtt újra próbálkozna.",
"required_data_missing": "Hiányzik a(z) szükséges %s adat",
"tfa_removal_blocked": "A kétfaktoros hitelesítés nem távolítható el, mert elengedhetetlen a fiókod használatához."
}
}

View File

@@ -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 é 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 é 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",

View File

@@ -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",

View File

@@ -27,7 +27,7 @@ if (isset($_SERVER['PHP_AUTH_USER'])) {
}
$login_check = check_login($username, $password, array('service' => $service));
if ($login_check === 'user' && hasACLAccess('sogo_access')) {
if ($login_check === 'user') {
header("X-User: $username");
header("X-Auth: Basic ".base64_encode("$username:$password"));
header("X-Auth-Type: Basic");
@@ -47,7 +47,6 @@ elseif (isset($_GET['login'])) {
// check permissions (if dual_login is active, deny sso when acl is not given)
$login = html_entity_decode(rawurldecode($_GET["login"]));
if (isset($_SESSION['mailcow_cc_role']) &&
hasACLAccess('sogo_access') &&
(($_SESSION['acl']['login_as'] == "1" && $ALLOW_ADMIN_EMAIL_LOGIN !== 0) || ($is_dual === false && $login == $_SESSION['mailcow_cc_username']))) {
if (filter_var($login, FILTER_VALIDATE_EMAIL)) {
if (user_get_alias_details($login) !== false) {

View File

@@ -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' %}

View 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>

View File

@@ -11,8 +11,8 @@
<link rel="stylesheet" href="{{ css_path }}">
<script>
// check if darkmode is preferred by OS or set by localStorage
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && localStorage.getItem("theme") !== "light" ||
localStorage.getItem("theme") === "dark") {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && localStorage.getItem("mailcow_theme") !== "light" ||
localStorage.getItem("mailcow_theme") === "dark") {
var head = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.id = 'dark-mode-theme';
@@ -193,7 +193,7 @@ $(window).scroll(function() {
});
// Select language and reopen active URL without POST
function setLang(sel) {
$.post( '{{ uri }}', {lang: sel} );
$.post( '{{ uri|escape("js") }}', {lang: sel} );
window.location.href = window.location.pathname + window.location.search;
}
// FIDO2 functions

View File

@@ -8,7 +8,7 @@
<input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="sogo_redirection">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
<div class="row mb-4">
@@ -125,7 +125,6 @@
<option value="syncjobs" {% if template.attributes.acl_syncjobs == '1' %} selected{% endif %}>{{ lang.acl["syncjobs"] }}</option>
<option value="eas_reset" {% if template.attributes.acl_eas_reset == '1' %} selected{% endif %}>{{ lang.acl["eas_reset"] }}</option>
<option value="sogo_profile_reset" {% if template.attributes.acl_sogo_profile_reset == '1' %} selected{% endif %}>{{ lang.acl["sogo_profile_reset"] }}</option>
<option value="sogo_access" {% if template.attributes.acl_sogo_access == '1' %} selected{% endif %}>{{ lang.acl["sogo_access"] }}</option>
<option value="pushover" {% if template.attributes.acl_pushover == '1' %} selected{% endif %}>{{ lang.acl["pushover"] }}</option>
<option value="quarantine" {% if template.attributes.acl_quarantine == '1' %} selected{% endif %}>{{ lang.acl["quarantine"] }}</option>
<option value="quarantine_attachments" {% if template.attributes.acl_quarantine_attachments == '1' %} selected{% endif %}>{{ lang.acl["quarantine_attachments"] }}</option>
@@ -170,8 +169,8 @@
<div class="row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_redirection"{% if template.attributes.sogo_redirection == '1' %} checked{% endif %}> {{ lang.edit.sogo_redirection }}</label>
<small class="text-muted">{{ lang.edit.sogo_redirection_info }}</small>
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_access"{% if template.attributes.sogo_access == '1' %} checked{% endif %}> {{ lang.edit.sogo_access }}</label>
<small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
</div>
</div>
</div>

View File

@@ -25,7 +25,7 @@
<input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="force_tfa">
<input type="hidden" value="0" name="sogo_redirection">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
<div class="row mb-2">
<label class="control-label col-sm-2">{{ lang.admin.iam }}</label>
@@ -327,11 +327,11 @@
</div>
</div>
{% if not skip_sogo %}
<div data-acl="{{ acl.sogo_redirection }}" class="row">
<div data-acl="{{ acl.sogo_access }}" class="row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_redirection"{% if result.attributes.sogo_redirection == '1' %} checked{% endif %}> {{ lang.edit.sogo_redirection }}</label>
<small class="text-muted">{{ lang.edit.sogo_redirection_info }}</small>
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_access"{% if result.attributes.sogo_access == '1' %} checked{% endif %}> {{ lang.edit.sogo_access }}</label>
<small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
</div>
</div>
</div>

View File

@@ -10,7 +10,7 @@
<form class="form-horizontal" data-cached-form="true" data-id="add_mailbox" role="form" autocomplete="off">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="force_tfa">
<input type="hidden" value="0" name="sogo_redirection">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
<input type="hidden" value="mailcow" name="authsource">
@@ -166,7 +166,6 @@
<option value="syncjobs">{{ lang.acl["syncjobs"] }}</option>
<option value="eas_reset" selected>{{ lang.acl["eas_reset"] }}</option>
<option value="sogo_profile_reset">{{ lang.acl["sogo_profile_reset"] }}</option>
<option value="sogo_access" selected>{{ lang.acl["sogo_access"] }}</option>
<option value="pushover" selected>{{ lang.acl["pushover"] }}</option>
<option value="quarantine" selected>{{ lang.acl["quarantine"] }}</option>
<option value="quarantine_attachments" selected>{{ lang.acl["quarantine_attachments"] }}</option>
@@ -218,8 +217,8 @@
<div class="row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_redirection" id="sogo_access"> {{ lang.edit.sogo_redirection }}</label>
<small class="text-muted">{{ lang.edit.sogo_redirection_info }}</small>
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_access" id="sogo_access"> {{ lang.edit.sogo_access }}</label>
<small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
</div>
</div>
</div>
@@ -248,7 +247,7 @@
<input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="force_tfa">
<input type="hidden" value="0" name="sogo_redirection">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
<div class="row mb-4">
@@ -417,8 +416,8 @@
<div class="row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_redirection"> {{ lang.edit.sogo_redirection }}</label>
<small class="text-muted">{{ lang.edit.sogo_redirection_info }}</small>
<label><input type="checkbox" class="form-check-input" value="1" name="sogo_access"> {{ lang.edit.sogo_access }}</label>
<small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
</div>
</div>
</div>

View File

@@ -23,14 +23,10 @@
<a href="/sogo-auth.php?login={{ mailcow_cc_username }}" role="button" class="btn btn-primary btn-lg btn-block btn-xs-lg w-100">
{{ lang.user.open_webmail_sso }} <i class="bi bi-arrow-right"></i>
</a>
{% elseif acl.sogo_access == 1 %}
{% else %}
<a href="/SOGo/so" role="button" class="btn btn-primary btn-lg btn-block btn-xs-lg w-100">
{{ lang.user.open_webmail_sso }} <i class="bi bi-arrow-right"></i>
</a>
{% else %}
<button disabled class="btn btn-secondary btn-block btn-xs-lg w-100">
{{ lang.user.open_webmail_sso }} <i class="bi bi-arrow-right"></i>
</button>
{% endif %}
</div>
</div>

View File

@@ -252,7 +252,7 @@ services:
- sogo
dovecot-mailcow:
image: ghcr.io/mailcow/dovecot:2.3.21.1-1
image: ghcr.io/mailcow/dovecot:2.3.21.1-3
depends_on:
- mysql-mailcow
- netfilter-mailcow
@@ -465,7 +465,7 @@ services:
condition: service_started
unbound-mailcow:
condition: service_healthy
image: ghcr.io/mailcow/acme:1.96
image: ghcr.io/mailcow/acme:1.97
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment: