Merge pull request #6535 from mailcow/staging

Update 2025-05
This commit is contained in:
FreddleSpl0it 2025-05-13 09:58:32 +02:00 committed by GitHub
commit 798e6a4c00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 687 additions and 202 deletions

View File

@ -32,6 +32,13 @@ import time
import magic import magic
import re import re
skip_olefy = os.getenv('SKIP_OLEFY', '')
if skip_olefy.lower() in ['yes', 'y']:
print("SKIP_OLEFY=y, skipping Olefy...")
time.sleep(365 * 24 * 60 * 60)
sys.exit(0)
# merge variables from /etc/olefy.conf and the defaults # merge variables from /etc/olefy.conf and the defaults
olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1') olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1')
olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050')) olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050'))

View File

@ -5,7 +5,7 @@ LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.24 ARG APCU_PECL_VERSION=5.1.24
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.7.0 ARG IMAGICK_PECL_VERSION=3.8.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.8 ARG MAILPARSE_PECL_VERSION=3.1.8
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$

View File

@ -81,6 +81,27 @@ EOF
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE
fi fi
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ -f /etc/rspamd/local.d/external_services.conf ]]; then
rm /etc/rspamd/local.d/external_services.conf
fi
else
cat <<EOF > /etc/rspamd/local.d/external_services.conf
oletools {
# default olefy settings
servers = "olefy:10055";
# needs to be set explicitly for Rspamd < 1.9.5
scan_mime_parts = true;
# mime-part regex matching in content-type or filename
# block all macros
extended = true;
max_size = 3145728;
timeout = 20.0;
retransmits = 1;
}
EOF
fi
# Provide additional lua modules # Provide additional lua modules
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so

View File

@ -994,6 +994,7 @@ PID=$!
echo "Spawned cert_checks with PID ${PID}" echo "Spawned cert_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID}) BACKGROUND_TASKS+=(${PID})
if [[ "${SKIP_OLEFY}" =~ ^([nN][oO]|[nN])+$ ]]; then
( (
while true; do while true; do
if ! olefy_checks; then if ! olefy_checks; then
@ -1005,6 +1006,7 @@ done
PID=$! PID=$!
echo "Spawned olefy_checks with PID ${PID}" echo "Spawned olefy_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID}) BACKGROUND_TASKS+=(${PID})
fi
( (
while true; do while true; do

View File

@ -79,7 +79,9 @@ if ($isSOGoRequest) {
} }
} }
if ($result === false){ if ($result === false){
$result = apppass_login($post['username'], $post['password'], array($post['service'] => true), array( // If it's a SOGo Request, don't check for protocol access
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
$result = apppass_login($post['username'], $post['password'], $service, array(
'is_internal' => true, 'is_internal' => true,
'remote_addr' => $post['real_rip'] 'remote_addr' => $post['real_rip']
)); ));

View File

@ -29,13 +29,23 @@ function auth_password_verify(request, password)
insecure = true insecure = true
} }
if c ~= 200 then -- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
if c ~= 200 and c ~= 401 then
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user) dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream error" return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
end end
local api_response = json.decode(table.concat(res)) local response_str = table.concat(res)
if api_response.success == true then local is_response_valid, response_json = pcall(json.decode, response_str)
if not is_response_valid then
dovecot.i_info("Invalid JSON received: " .. response_str)
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Invalid response format"
end
if response_json.success == true then
return dovecot.auth.PASSDB_RESULT_OK, "" return dovecot.auth.PASSDB_RESULT_OK, ""
end end

View File

@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Tue Apr 1 00:20:51 UTC 2025 # Whitelist generated by Postwhite v3.4 on Thu May 1 00:21:10 UTC 2025
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2067 total rules # 2058 total rules
2a00:1450:4000::/36 permit 2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit 2a01:111:f400::/48 permit
2a01:111:f403:8000::/50 permit 2a01:111:f403:8000::/50 permit
@ -18,6 +18,7 @@
2a02:a60:0:5::/64 permit 2a02:a60:0:5::/64 permit
2c0f:fb50:4000::/36 permit 2c0f:fb50:4000::/36 permit
2.207.151.53 permit 2.207.151.53 permit
2.207.217.30 permit
3.70.123.177 permit 3.70.123.177 permit
3.93.157.0/24 permit 3.93.157.0/24 permit
3.94.40.108 permit 3.94.40.108 permit
@ -26,10 +27,8 @@
8.20.114.31 permit 8.20.114.31 permit
8.25.194.0/23 permit 8.25.194.0/23 permit
8.25.196.0/23 permit 8.25.196.0/23 permit
8.39.54.0/23 permit 8.36.116.0/24 permit
8.39.54.250/31 permit 8.39.144.0/24 permit
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit 12.130.86.238 permit
13.107.246.59 permit 13.107.246.59 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
@ -103,6 +102,7 @@
27.123.206.80/28 permit 27.123.206.80/28 permit
31.25.48.222 permit 31.25.48.222 permit
31.47.251.17 permit 31.47.251.17 permit
31.186.239.0/24 permit
34.2.64.0/22 permit 34.2.64.0/22 permit
34.2.68.0/23 permit 34.2.68.0/23 permit
34.2.70.0/23 permit 34.2.70.0/23 permit
@ -121,6 +121,10 @@
34.2.90.0/23 permit 34.2.90.0/23 permit
34.2.92.0/23 permit 34.2.92.0/23 permit
34.2.94.0/23 permit 34.2.94.0/23 permit
34.70.158.162 permit
34.74.74.140 permit
34.83.159.189 permit
34.141.160.224 permit
34.195.217.107 permit 34.195.217.107 permit
34.212.163.75 permit 34.212.163.75 permit
34.215.104.144 permit 34.215.104.144 permit
@ -132,6 +136,7 @@
35.190.247.0/24 permit 35.190.247.0/24 permit
35.191.0.0/16 permit 35.191.0.0/16 permit
35.205.92.9 permit 35.205.92.9 permit
35.228.216.85 permit
35.242.169.159 permit 35.242.169.159 permit
37.188.97.188 permit 37.188.97.188 permit
37.218.248.47 permit 37.218.248.47 permit
@ -233,6 +238,7 @@
52.95.49.88/29 permit 52.95.49.88/29 permit
52.96.91.34 permit 52.96.91.34 permit
52.96.111.82 permit 52.96.111.82 permit
52.96.172.98 permit
52.96.214.50 permit 52.96.214.50 permit
52.96.222.194 permit 52.96.222.194 permit
52.96.222.226 permit 52.96.222.226 permit
@ -272,7 +278,6 @@
54.244.54.130 permit 54.244.54.130 permit
54.244.242.0/24 permit 54.244.242.0/24 permit
54.255.61.23 permit 54.255.61.23 permit
56.124.6.228 permit
57.103.64.0/18 permit 57.103.64.0/18 permit
62.13.128.0/24 permit 62.13.128.0/24 permit
62.13.129.128/25 permit 62.13.129.128/25 permit
@ -309,9 +314,6 @@
64.207.219.13 permit 64.207.219.13 permit
64.207.219.14 permit 64.207.219.14 permit
64.207.219.15 permit 64.207.219.15 permit
64.207.219.24 permit
64.207.219.25 permit
64.207.219.26 permit
64.207.219.71 permit 64.207.219.71 permit
64.207.219.72 permit 64.207.219.72 permit
64.207.219.73 permit 64.207.219.73 permit
@ -321,9 +323,6 @@
64.207.219.77 permit 64.207.219.77 permit
64.207.219.78 permit 64.207.219.78 permit
64.207.219.79 permit 64.207.219.79 permit
64.207.219.88 permit
64.207.219.89 permit
64.207.219.90 permit
64.207.219.135 permit 64.207.219.135 permit
64.207.219.136 permit 64.207.219.136 permit
64.207.219.137 permit 64.207.219.137 permit
@ -359,7 +358,6 @@
65.110.161.77 permit 65.110.161.77 permit
65.123.29.213 permit 65.123.29.213 permit
65.123.29.220 permit 65.123.29.220 permit
65.154.166.0/24 permit
65.212.180.36 permit 65.212.180.36 permit
66.102.0.0/20 permit 66.102.0.0/20 permit
66.119.150.192/26 permit 66.119.150.192/26 permit
@ -1323,9 +1321,6 @@
117.120.16.0/21 permit 117.120.16.0/21 permit
119.42.242.52/31 permit 119.42.242.52/31 permit
119.42.242.156 permit 119.42.242.156 permit
121.244.91.48 permit
121.244.91.52 permit
122.15.156.182 permit
123.126.78.64/29 permit 123.126.78.64/29 permit
124.108.96.24/31 permit 124.108.96.24/31 permit
124.108.96.28/31 permit 124.108.96.28/31 permit
@ -1388,21 +1383,7 @@
134.170.141.64/26 permit 134.170.141.64/26 permit
134.170.143.0/24 permit 134.170.143.0/24 permit
134.170.174.0/24 permit 134.170.174.0/24 permit
135.84.80.0/24 permit
135.84.81.0/24 permit
135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit 135.84.216.0/22 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.162.0/24 permit
136.143.176.0/24 permit
136.143.177.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
136.143.190.0/23 permit
136.147.128.0/20 permit 136.147.128.0/20 permit
136.147.135.0/24 permit 136.147.135.0/24 permit
136.147.176.0/20 permit 136.147.176.0/20 permit
@ -1417,7 +1398,6 @@
139.138.46.219 permit 139.138.46.219 permit
139.138.57.55 permit 139.138.57.55 permit
139.138.58.119 permit 139.138.58.119 permit
139.167.79.86 permit
139.180.17.0/24 permit 139.180.17.0/24 permit
140.238.148.191 permit 140.238.148.191 permit
141.148.159.229 permit 141.148.159.229 permit
@ -1452,6 +1432,7 @@
146.20.215.0/24 permit 146.20.215.0/24 permit
146.20.215.182 permit 146.20.215.182 permit
146.88.28.0/24 permit 146.88.28.0/24 permit
146.148.116.76 permit
147.154.32.0/25 permit 147.154.32.0/25 permit
147.243.1.47 permit 147.243.1.47 permit
147.243.1.48 permit 147.243.1.48 permit
@ -1533,11 +1514,10 @@
163.114.132.120 permit 163.114.132.120 permit
163.114.134.16 permit 163.114.134.16 permit
163.114.135.16 permit 163.114.135.16 permit
163.116.128.0/17 permit
164.152.23.32 permit 164.152.23.32 permit
164.152.25.241 permit
164.177.132.168/30 permit 164.177.132.168/30 permit
165.173.128.0/24 permit
165.173.180.250/31 permit
165.173.182.250/31 permit
166.78.68.0/22 permit 166.78.68.0/22 permit
166.78.68.221 permit 166.78.68.221 permit
166.78.69.169 permit 166.78.69.169 permit
@ -1566,12 +1546,6 @@
168.245.12.252 permit 168.245.12.252 permit
168.245.46.9 permit 168.245.46.9 permit
168.245.127.231 permit 168.245.127.231 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.142.10 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
170.10.128.0/24 permit 170.10.128.0/24 permit
170.10.129.0/24 permit 170.10.129.0/24 permit
170.10.132.56/29 permit 170.10.132.56/29 permit
@ -1697,6 +1671,21 @@
193.123.56.63 permit 193.123.56.63 permit
194.19.134.0/25 permit 194.19.134.0/25 permit
194.64.234.129 permit 194.64.234.129 permit
194.97.196.0/24 permit
194.97.196.3 permit
194.97.196.4 permit
194.97.196.11 permit
194.97.196.12 permit
194.97.204.0/24 permit
194.97.204.3 permit
194.97.204.4 permit
194.97.204.11 permit
194.97.204.12 permit
194.97.212.0/24 permit
194.97.212.3 permit
194.97.212.4 permit
194.97.212.11 permit
194.97.212.12 permit
194.106.220.0/23 permit 194.106.220.0/23 permit
194.113.24.0/22 permit 194.113.24.0/22 permit
194.154.193.192/27 permit 194.154.193.192/27 permit
@ -1733,15 +1722,7 @@
199.16.156.0/22 permit 199.16.156.0/22 permit
199.33.145.1 permit 199.33.145.1 permit
199.33.145.32 permit 199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit 199.59.148.0/22 permit
199.67.80.2 permit
199.67.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.101.161.130 permit 199.101.161.130 permit
199.101.162.0/25 permit 199.101.162.0/25 permit
199.122.120.0/21 permit 199.122.120.0/21 permit
@ -1798,8 +1779,6 @@
204.92.114.187 permit 204.92.114.187 permit
204.92.114.203 permit 204.92.114.203 permit
204.92.114.204/31 permit 204.92.114.204/31 permit
204.141.32.0/23 permit
204.141.42.0/23 permit
204.220.160.0/21 permit 204.220.160.0/21 permit
204.220.168.0/21 permit 204.220.168.0/21 permit
204.220.176.0/20 permit 204.220.176.0/20 permit
@ -2046,15 +2025,27 @@
2001:0868:0100:0600::/64 permit 2001:0868:0100:0600::/64 permit
2001:4860:4000::/36 permit 2001:4860:4000::/36 permit
2001:748:100:40::2:0/112 permit 2001:748:100:40::2:0/112 permit
2001:748:400:1300::3 permit
2001:748:400:1300::4 permit
2001:748:400:1301::0/64 permit
2001:748:400:1301::3 permit
2001:748:400:1301::4 permit
2001:748:400:2300::3 permit
2001:748:400:2300::4 permit
2001:748:400:2301::0/64 permit
2001:748:400:2301::3 permit
2001:748:400:2301::4 permit
2001:748:400:3300::3 permit
2001:748:400:3300::4 permit
2001:748:400:3301::0/64 permit
2001:748:400:3301::3 permit
2001:748:400:3301::4 permit
2404:6800:4000::/36 permit 2404:6800:4000::/36 permit
2603:1010:3:3::5b permit 2603:1010:3:3::5b permit
2603:1020:201:10::10f permit 2603:1020:201:10::10f permit
2603:1030:20e:3::23c permit 2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit 2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit 2603:1030:c02:8::14 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:4000::/36 permit
2620:109:c003:104::/64 permit 2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit 2620:109:c003:104::215 permit

View File

@ -24,7 +24,6 @@
/.+\.guru$/i /.+\.guru$/i
/.+\.icu$/i /.+\.icu$/i
/.+\.id$/i /.+\.id$/i
/.+\.info$/i
/.+\.in.net$/i /.+\.in.net$/i
/.+\.ir$/i /.+\.ir$/i
/.+\.jetzt$/i /.+\.jetzt$/i

View File

@ -8,7 +8,7 @@ VIRUS_FOUND {
} }
# Bad policy from free mail providers # Bad policy from free mail providers
FREEMAIL_POLICY_FAILURE { FREEMAIL_POLICY_FAILURE {
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies"; expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST & !WHITELISTED_FWD_HOST & -g+:policies";
score = 16.0; score = 16.0;
} }
# Applies to freemail with undisclosed recipients # Applies to freemail with undisclosed recipients

View File

@ -1,12 +0,0 @@
oletools {
# default olefy settings
servers = "olefy:10055";
# needs to be set explicitly for Rspamd < 1.9.5
scan_mime_parts = true;
# mime-part regex matching in content-type or filename
# block all macros
extended = true;
max_size = 3145728;
timeout = 20.0;
retransmits = 1;
}

View File

@ -16,6 +16,9 @@
SOGoFoldersSendEMailNotifications = YES; SOGoFoldersSendEMailNotifications = YES;
SOGoForwardEnabled = YES; SOGoForwardEnabled = YES;
// Added with SOGo 5.12 - Allows users to cleanup there maildirectories by deleting mails oder than X
SOGoEnableMailCleaning = YES;
// Fixes "MODIFICATION_FAILED" error (HTTP 412) in Clients when accepting invitations from external services // Fixes "MODIFICATION_FAILED" error (HTTP 412) in Clients when accepting invitations from external services
SOGoDisableOrganizerEventCheck = YES; SOGoDisableOrganizerEventCheck = YES;
@ -91,7 +94,7 @@
//SoDebugBaseURL = YES; //SoDebugBaseURL = YES;
//ImapDebugEnabled = YES; //ImapDebugEnabled = YES;
//SOGoEASDebugEnabled = YES; //SOGoEASDebugEnabled = YES;
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10 SOGoEASSearchInBody = YES;
//LDAPDebugEnabled = YES; //LDAPDebugEnabled = YES;
//PGDebugEnabled = YES; //PGDebugEnabled = YES;
//MySQL4DebugEnabled = YES; //MySQL4DebugEnabled = YES;

View File

@ -18,6 +18,7 @@ elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] !=
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true; $clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true;
$olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true;
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
@ -33,6 +34,7 @@ $vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', '
// containers // containers
$containers_info = (array) docker('info'); $containers_info = (array) docker('info');
if ($clamd_status === false) unset($containers_info['clamd-mailcow']); if ($clamd_status === false) unset($containers_info['clamd-mailcow']);
if ($olefy_status === false) unset($containers_info['olefy-mailcow']);
ksort($containers_info); ksort($containers_info);
$containers = array(); $containers = array();
foreach ($containers_info as $container => $container_info) { foreach ($containers_info as $container => $container_info) {
@ -77,6 +79,7 @@ $template_data = [
'gal' => @$_SESSION['gal'], 'gal' => @$_SESSION['gal'],
'license_guid' => license('guid'), 'license_guid' => license('guid'),
'clamd_status' => $clamd_status, 'clamd_status' => $clamd_status,
'olefy_status' => $olefy_status,
'containers' => $containers, 'containers' => $containers,
'ip_check' => customize('get', 'ip_check'), 'ip_check' => customize('get', 'ip_check'),
'lang_admin' => json_encode($lang['admin']), 'lang_admin' => json_encode($lang['admin']),

View File

@ -22,7 +22,8 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
$template = 'admin_index.twig'; $template = 'admin_index.twig';
$template_data = [ $template_data = [
'login_delay' => @$_SESSION['ldelay'] 'login_delay' => @$_SESSION['ldelay'],
'custom_login' => customize('get', 'custom_login'),
]; ];
$js_minifier->add('/web/js/site/index.js'); $js_minifier->add('/web/js/site/index.js');

View File

@ -125,6 +125,7 @@ $template_data = [
'logo_specs' => customize('get', 'main_logo_specs'), 'logo_specs' => customize('get', 'main_logo_specs'),
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'), 'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
'ip_check' => customize('get', 'ip_check'), 'ip_check' => customize('get', 'ip_check'),
'custom_login' => customize('get', 'custom_login'),
'password_complexity' => password_complexity('get'), 'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'cors_settings' => $cors_settings, 'cors_settings' => $cors_settings,

View File

@ -5847,6 +5847,7 @@ paths:
client_id: "mailcow_client" client_id: "mailcow_client"
client_secret: "*" client_secret: "*"
redirect_url: "https://mail.mailcow.tld" redirect_url: "https://mail.mailcow.tld"
redirect_url_extra: ["https://extramail.mailcow.tld"]
version: "26.1.3" version: "26.1.3"
default_template: "Default" default_template: "Default"
mappers: mappers:
@ -5900,6 +5901,9 @@ paths:
redirect_url: redirect_url:
description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc. description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc.
type: string type: string
redirect_url_extra:
description: Additional redirect URLs that OIDC Provider can use after authentication if valid.
type: array
version: version:
description: Specifies the Keycloak version. Required if `authsource` is keycloak. description: Specifies the Keycloak version. Required if `authsource` is keycloak.
type: string type: string
@ -5990,6 +5994,7 @@ paths:
client_id: "mailcow_client" client_id: "mailcow_client"
client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf" client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
redirect_url: "https://mail.mailcow.tld" redirect_url: "https://mail.mailcow.tld"
redirect_url_extra: ["https://extramail.mailcow.tld"]
version: "26.1.3" version: "26.1.3"
default_template: "Default" default_template: "Default"
mappers: ["small_mbox", "medium_mbox"] mappers: ["small_mbox", "medium_mbox"]
@ -6034,6 +6039,7 @@ paths:
client_id: "mailcow_client" client_id: "mailcow_client"
client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf" client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
redirect_url: "https://mail.mailcow.tld" redirect_url: "https://mail.mailcow.tld"
redirect_url_extra: ["https://extramail.mailcow.tld"]
client_scopes: "openid profile email mailcow_template" client_scopes: "openid profile email mailcow_template"
default_template: "Default" default_template: "Default"
mappers: ["small_mbox", "medium_mbox"] mappers: ["small_mbox", "medium_mbox"]

View File

@ -85,7 +85,7 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?>
<authentication>password-cleartext</authentication> <authentication>password-cleartext</authentication>
</outgoingServer> </outgoingServer>
<enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin.php"> <enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin">
<instruction>If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now.</instruction> <instruction>If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now.</instruction>
<instruction lang="de">Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden.</instruction> <instruction lang="de">Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden.</instruction>
</enable> </enable>

View File

@ -22,6 +22,7 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
$template = 'domainadmin_index.twig'; $template = 'domainadmin_index.twig';
$template_data = [ $template_data = [
'login_delay' => @$_SESSION['ldelay'], 'login_delay' => @$_SESSION['ldelay'],
'custom_login' => customize('get', 'custom_login'),
]; ];
$js_minifier->add('/web/js/site/index.js'); $js_minifier->add('/web/js/site/index.js');

View File

@ -242,6 +242,7 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
$row['attributes'] = json_decode($row['attributes'], true);
switch ($row['authsource']) { switch ($row['authsource']) {
case 'keycloak': case 'keycloak':
// user authsource is keycloak, try using via rest flow // user authsource is keycloak, try using via rest flow
@ -351,6 +352,11 @@ function user_login($user, $pass, $extra = null){
} }
// verify password // verify password
if (verify_hash($row['password'], $pass) !== false) { if (verify_hash($row['password'], $pass) !== false) {
if (intval($row['attributes']['force_pw_update']) == 1) {
$_SESSION['pending_pw_update'] = true;
}
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
@ -471,6 +477,9 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
} }
return false; return false;
} }
if (!$iam_provider) {
return false;
}
// get access_token for service account of mailcow client // get access_token for service account of mailcow client
$admin_token = identity_provider("get-keycloak-admin-token"); $admin_token = identity_provider("get-keycloak-admin-token");
@ -540,6 +549,17 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
return 'user'; return 'user';
} }
// check if login provisioning is enabled before creating user
if (!$iam_settings['login_provisioning']){
if (!$is_internal){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
}
return false;
}
// check if matching attribute exist // check if matching attribute exist
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) { if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
if (!empty($iam_settings['default_template'])) { if (!empty($iam_settings['default_template'])) {
@ -653,10 +673,21 @@ function ldap_mbox_login($user, $pass, $extra = null){
return 'user'; return 'user';
} }
// check if login provisioning is enabled before creating user
if (!$iam_settings['login_provisioning']){
if (!$is_internal){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
}
return false;
}
// check if matching attribute exist // check if matching attribute exist
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) { if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
if (!empty($iam_settings['default_tempalte'])) { if (!empty($iam_settings['default_template'])) {
$mbox_template = $iam_settings['default_tempalte']; $mbox_template = $iam_settings['default_template'];
} else { } else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',

View File

@ -204,6 +204,35 @@ function customize($_action, $_item, $_data = null) {
'msg' => 'ip_check_opt_in_modified' 'msg' => 'ip_check_opt_in_modified'
); );
break; break;
case 'custom_login':
$hide_user_quicklink = ($_data['hide_user_quicklink'] == "1") ? 1 : 0;
$hide_domainadmin_quicklink = ($_data['hide_domainadmin_quicklink'] == "1") ? 1 : 0;
$hide_admin_quicklink = ($_data['hide_admin_quicklink'] == "1") ? 1 : 0;
$force_sso = ($_data['force_sso'] == "1") ? 1 : 0;
$custom_login = array(
"hide_user_quicklink" => $hide_user_quicklink,
"hide_domainadmin_quicklink" => $hide_domainadmin_quicklink,
"hide_admin_quicklink" => $hide_admin_quicklink,
"force_sso" => $force_sso,
);
try {
$redis->set('CUSTOM_LOGIN', json_encode($custom_login));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'custom_login_modified'
);
break;
} }
break; break;
case 'delete': case 'delete':
@ -357,6 +386,20 @@ function customize($_action, $_item, $_data = null) {
return false; return false;
} }
break; break;
case 'custom_login':
try {
$custom_login = ($custom_login = $redis->get('CUSTOM_LOGIN')) ? $custom_login : array();
return json_decode($custom_login, true);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
break;
} }
break; break;
} }

View File

@ -1001,6 +1001,7 @@ function edit_user_account($_data) {
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username' => $username ':username' => $username
)); ));
$_SESSION['pending_pw_update'] = false;
update_sogo_static_view(); update_sogo_static_view();
} }
@ -2286,12 +2287,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $row){ foreach($rows as $row){
switch ($row["key"]) { switch ($row["key"]) {
case "redirect_url_extra":
case "mappers": case "mappers":
case "templates": case "templates":
$settings[$row["key"]] = json_decode($row["value"]); $settings[$row["key"]] = json_decode($row["value"]);
break; break;
case "use_ssl": case "use_ssl":
case "use_tls": case "use_tls":
case "login_provisioning":
case "ignore_ssl_errors": case "ignore_ssl_errors":
$settings[$row["key"]] = boolval($row["value"]); $settings[$row["key"]] = boolval($row["value"]);
break; break;
@ -2300,6 +2303,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
break; break;
} }
} }
// set login_provisioning if not exists
if (!array_key_exists('login_provisioning', $settings)) {
$settings['login_provisioning'] = 1;
}
// return default client_scopes for generic-oidc if none is set // return default client_scopes for generic-oidc if none is set
if ($settings["authsource"] == "generic-oidc" && empty($settings["client_scopes"])){ if ($settings["authsource"] == "generic-oidc" && empty($settings["client_scopes"])){
$settings["client_scopes"] = "openid profile email mailcow_template"; $settings["client_scopes"] = "openid profile email mailcow_template";
@ -2365,6 +2372,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
} }
$_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false; $_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
$_data['login_provisioning'] = isset($_data['login_provisioning']) ? boolval($_data['login_provisioning']) : false;
switch ($_data['authsource']) { switch ($_data['authsource']) {
case "keycloak": case "keycloak":
$_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null; $_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
@ -2373,14 +2381,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0; $_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15; $_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval']; $_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error'); $required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error', 'login_provisioning');
break; break;
case "generic-oidc": case "generic-oidc":
$_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null; $_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
$_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null; $_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
$_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null; $_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
$_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email mailcow_template"; $_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email mailcow_template";
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error'); $required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error', 'login_provisioning');
break; break;
case "ldap": case "ldap":
$_data['host'] = (!empty($_data['host'])) ? str_replace(" ", "", $_data['host']) : ""; $_data['host'] = (!empty($_data['host'])) ? str_replace(" ", "", $_data['host']) : "";
@ -2394,7 +2402,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false; $_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15; $_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval']; $_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error'); $required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error', 'login_provisioning');
break; break;
} }
@ -2418,6 +2426,18 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
} }
$pdo->commit(); $pdo->commit();
// add redirect_url_extra
if (isset($_data['redirect_url_extra'])){
$_data['redirect_url_extra'] = (!is_array($_data['redirect_url_extra'])) ? array($_data['redirect_url_extra']) : $_data['redirect_url_extra'];
$redirect_url_extra = array_filter($_data['redirect_url_extra']);
$redirect_url_extra = json_encode($redirect_url_extra);
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('redirect_url_extra', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
$stmt->bindParam(':value', $redirect_url_extra);
$stmt->execute();
}
// add default template // add default template
if (isset($_data['default_template'])) { if (isset($_data['default_template'])) {
$_data['default_template'] = (empty($_data['default_template'])) ? "" : $_data['default_template']; $_data['default_template'] = (empty($_data['default_template'])) ? "" : $_data['default_template'];
@ -2752,6 +2772,16 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
return true; return true;
} }
// user doesn't exist, check if login provisioning is enabled
if (!$iam_settings['login_provisioning']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
return false;
}
if (empty($iam_settings['mappers']) || empty($user_template) || $mapper_key === false){ if (empty($iam_settings['mappers']) || empty($user_template) || $mapper_key === false){
if (!empty($iam_settings['default_template'])) { if (!empty($iam_settings['default_template'])) {
$mbox_template = $iam_settings['default_template']; $mbox_template = $iam_settings['default_template'];
@ -2851,7 +2881,19 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
case "get-redirect": case "get-redirect":
if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc') if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc')
return false; return false;
$authUrl = $iam_provider->getAuthorizationUrl(); $options = [];
if (isset($iam_settings['redirect_url_extra'])) {
// check if the current domain is used in an extra redirect URL
$targetDomain = strtolower($_SERVER['HTTP_HOST']);
foreach ($iam_settings['redirect_url_extra'] as $testUrl) {
$testUrlParsed = parse_url($testUrl);
if (isset($testUrlParsed['host']) && strtolower($testUrlParsed['host']) == $targetDomain) {
$options['redirect_uri'] = $testUrl;
break;
}
}
}
$authUrl = $iam_provider->getAuthorizationUrl($options);
$_SESSION['oauth2state'] = $iam_provider->getState(); $_SESSION['oauth2state'] = $iam_provider->getState();
return $authUrl; return $authUrl;
break; break;

View File

@ -169,7 +169,7 @@ function quarantine($_action, $_data = null) {
} }
} }
elseif ($release_format == 'raw') { elseif ($release_format == 'raw') {
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $detail_row['msg']); $detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
$postfix_talk = array( $postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)), array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)), array('250', 'MAIL FROM: ' . $sender . chr(10)),
@ -464,7 +464,7 @@ function quarantine($_action, $_data = null) {
} }
} }
elseif ($release_format == 'raw') { elseif ($release_format == 'raw') {
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $row['msg']); $row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
$postfix_talk = array( $postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)), array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)), array('250', 'MAIL FROM: ' . $sender . chr(10)),

View File

@ -76,7 +76,10 @@ if (isset($_POST["verify_tfa_login"])) {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
die(); die();
} else { } else {
@ -139,7 +142,10 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$user_details = mailbox("get", "mailbox_details", $login_user); $user_details = mailbox("get", "mailbox_details", $login_user);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/{$login_user}"); header("Location: /SOGo/so/{$login_user}");
die(); die();
} else { } else {

View File

@ -11,7 +11,7 @@ if (isset($_SESSION['mailcow_cc_role']) && isset($_SESSION['oauth2_request'])) {
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
} else { } else {
header("Location: /user"); header("Location: /user");
@ -33,16 +33,18 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
$has_iam_sso = false; $has_iam_sso = false;
if ($iam_provider){ if ($iam_provider){
$has_iam_sso = identity_provider("get-redirect") ? true : false; $iam_redirect_url = identity_provider("get-redirect");
$has_iam_sso = $iam_redirect_url ? true : false;
} }
$custom_login = customize('get', 'custom_login');
$template = 'user_index.twig'; $template = 'user_index.twig';
$template_data = [ $template_data = [
'oauth2_request' => @$_SESSION['oauth2_request'], 'oauth2_request' => @$_SESSION['oauth2_request'],
'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'), 'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
'login_delay' => @$_SESSION['ldelay'], 'login_delay' => @$_SESSION['ldelay'],
'has_iam_sso' => $has_iam_sso 'has_iam_sso' => $has_iam_sso,
'custom_login' => $custom_login,
]; ];
$js_minifier->add('/web/js/site/index.js'); $js_minifier->add('/web/js/site/index.js');

View File

@ -51,7 +51,7 @@ jQuery(function($){
$('.submit_rspamd_regex').attr({"disabled": true}); $('.submit_rspamd_regex').attr({"disabled": true});
}); });
$("#show_rspamd_global_filters").click(function() { $("#show_rspamd_global_filters").click(function() {
$.get("inc/ajax/show_rspamd_global_filters.php"); $.get("/inc/ajax/show_rspamd_global_filters.php");
$("#confirm_show_rspamd_global_filters").hide(); $("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("d-none"); $("#rspamd_global_filters").removeClass("d-none");
}); });
@ -558,7 +558,7 @@ jQuery(function($){
} else if (table == 'oauth2clientstable') { } else if (table == 'oauth2clientstable') {
$.each(data, function (i, item) { $.each(data, function (i, item) {
item.action = '<div class="btn-group">' + item.action = '<div class="btn-group">' +
'<a href="/edit.php?oauth2client=' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' + '<a href="/edit/oauth2client/' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-oauth2-client" data-api-url="delete/oauth2-client" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' + '<a href="#" data-action="delete_selected" data-id="single-oauth2-client" data-api-url="delete/oauth2-client" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>'; '</div>';
item.scope = "profile"; item.scope = "profile";
@ -573,7 +573,7 @@ jQuery(function($){
item.action = '<div class="btn-group">' + item.action = '<div class="btn-group">' +
'<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' + '<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' + '<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-success"><i class="bi bi-person-fill"></i> Login</a>' + '<a href="/domainadmin/?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-success"><i class="bi bi-person-fill"></i> Login</a>' +
'</div>'; '</div>';
}); });
} else if (table == 'adminstable') { } else if (table == 'adminstable') {
@ -655,7 +655,7 @@ jQuery(function($){
$(this).html('<i class="bi bi-arrow-repeat icon-spin"></i> '); $(this).html('<i class="bi bi-arrow-repeat icon-spin"></i> ');
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: 'inc/ajax/relay_check.php', url: '/inc/ajax/relay_check.php',
dataType: 'text', dataType: 'text',
data: $('#test_relayhost_form').serialize(), data: $('#test_relayhost_form').serialize(),
complete: function (data) { complete: function (data) {
@ -789,6 +789,18 @@ jQuery(function($){
$('.iam_ldap_rolemap_del').click(async function(e){ $('.iam_ldap_rolemap_del').click(async function(e){
deleteAttributeMappingRow(this, e); deleteAttributeMappingRow(this, e);
}); });
$('.iam_redirect_add_keycloak').click(async function(e){
addRedirectUrlRow('#iam_keycloak_redirect_list', '.iam_keycloak_redirect_del', e);
});
$('.iam_redirect_add_generic').click(async function(e){
addRedirectUrlRow('#iam_generic_redirect_list', '.iam_generic_redirect_del', e);
});
$('.iam_keycloak_redirect_del').click(async function(e){
deleteRedirectUrlRow(this, e);
});
$('.iam_generic_redirect_del').click(async function(e){
deleteRedirectUrlRow(this, e);
});
// selecting identity provider // selecting identity provider
$('#iam_provider').on('change', function(){ $('#iam_provider').on('change', function(){
// toggle password fields // toggle password fields
@ -833,4 +845,22 @@ jQuery(function($){
if ($(elem).parent().parent().parent().parent().children().length > 1) if ($(elem).parent().parent().parent().parent().children().length > 1)
$(elem).parent().parent().parent().remove(); $(elem).parent().parent().parent().remove();
} }
function addRedirectUrlRow(list_id, del_class, e) {
e.preventDefault();
var parent = $(list_id)
$(parent).children().last().clone().appendTo(parent);
var newChild = $(parent).children().last();
$(newChild).find('input').val('');
$(del_class).off('click');
$(del_class).click(async function(e){
deleteRedirectUrlRow(this, e);
});
}
function deleteRedirectUrlRow(elem, e) {
e.preventDefault();
if ($(elem).parent().parent().parent().parent().children().length > 2)
$(elem).parent().parent().parent().remove();
}
}); });

View File

@ -98,7 +98,7 @@ 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 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 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-app-indicator"></i> ' + escapeHtml(item.app_password_name || "App") + '</a>' : ''; var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-app-indicator"></i> ' + escapeHtml(item.app_password_name || "App") + '</a>' : '';
var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + "</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 ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
var ip_data = real_rip + ip_location + app_password; var ip_data = real_rip + ip_location + app_password;

View File

@ -1976,6 +1976,9 @@ if (isset($_GET['query'])) {
case "ip_check": case "ip_check":
process_edit_return(customize('edit', 'ip_check', $attr)); process_edit_return(customize('edit', 'ip_check', $attr));
break; break;
case "custom_login":
process_edit_return(customize('edit', 'custom_login', $attr));
break;
case "self": case "self":
if ($_SESSION['mailcow_cc_role'] == "domainadmin") { if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
process_edit_return(domain_admin('edit', $attr)); process_edit_return(domain_admin('edit', $attr));

View File

@ -0,0 +1 @@
{}

View File

@ -134,6 +134,7 @@
"admin_domains": "Domain-Zuweisungen", "admin_domains": "Domain-Zuweisungen",
"admins": "Administratoren", "admins": "Administratoren",
"admins_ldap": "LDAP-Administratoren", "admins_ldap": "LDAP-Administratoren",
"admin_quicklink": "Quicklink zur Admin-Loginseite ausblenden",
"advanced_settings": "Erweiterte Einstellungen", "advanced_settings": "Erweiterte Einstellungen",
"api_allow_from": "IP-Adressen oder Netzwerke (CIDR Notation) für Zugriff auf API", "api_allow_from": "IP-Adressen oder Netzwerke (CIDR Notation) für Zugriff auf API",
"api_info": "Die API befindet sich noch in Entwicklung, die Dokumentation kann unter <a href=\"/api\">/api</a> abgerufen werden.", "api_info": "Die API befindet sich noch in Entwicklung, die Dokumentation kann unter <a href=\"/api\">/api</a> abgerufen werden.",
@ -155,6 +156,7 @@
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.", "credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
"customer_id": "Kunde", "customer_id": "Kunde",
"customize": "UI-Anpassung", "customize": "UI-Anpassung",
"login_page": "Login-Seite",
"destination": "Ziel", "destination": "Ziel",
"dkim_add_key": "ARC/DKIM-Key hinzufügen", "dkim_add_key": "ARC/DKIM-Key hinzufügen",
"dkim_domains_selector": "Selector", "dkim_domains_selector": "Selector",
@ -173,6 +175,7 @@
"domain": "Domain", "domain": "Domain",
"domain_admin": "Administrator hinzufügen", "domain_admin": "Administrator hinzufügen",
"domain_admins": "Domain-Administratoren", "domain_admins": "Domain-Administratoren",
"domainadmin_quicklink": "Quicklink zur Domainadmin-Loginseite ausblenden",
"domain_s": "Domain(s)", "domain_s": "Domain(s)",
"duplicate": "Duplizieren", "duplicate": "Duplizieren",
"duplicate_dkim": "DKIM duplizieren", "duplicate_dkim": "DKIM duplizieren",
@ -195,6 +198,8 @@
"f2b_retry_window": "Wiederholungen im Zeitraum von (s)", "f2b_retry_window": "Wiederholungen im Zeitraum von (s)",
"f2b_whitelist": "Whitelist für Netzwerke und Hosts", "f2b_whitelist": "Whitelist für Netzwerke und Hosts",
"filter_table": "Tabelle filtern", "filter_table": "Tabelle filtern",
"force_sso_text": "Wenn ein externer OIDC-Provider konfiguriert ist, blendet diese Option die mailcow Loginform aus und zeigt nur den Single Sign-On-Button an.",
"force_sso": "mailcow Login deaktivieren und nur Single Sign-On anzeigen",
"forwarding_hosts": "Weiterleitungs-Hosts", "forwarding_hosts": "Weiterleitungs-Hosts",
"forwarding_hosts_add_hint": "Sie können entweder IPv4-/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.", "forwarding_hosts_add_hint": "Sie können entweder IPv4-/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.",
"forwarding_hosts_hint": "Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt, optional kann er aber in den Spam-Ordner einsortiert werden. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem mailcow-Server eingerichtet wurde.", "forwarding_hosts_hint": "Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt, optional kann er aber in den Spam-Ordner einsortiert werden. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem mailcow-Server eingerichtet wurde.",
@ -222,6 +227,7 @@
"iam_host": "Host", "iam_host": "Host",
"iam_host_info": "Gib einen oder mehrere LDAP-Hosts ein, getrennt durch Kommas.", "iam_host_info": "Gib einen oder mehrere LDAP-Hosts ein, getrennt durch Kommas.",
"iam_import_users": "Importiere Benutzer", "iam_import_users": "Importiere Benutzer",
"iam_login_provisioning": "Benutzer beim Login erstellen",
"iam_mapping": "Attribut Mapping", "iam_mapping": "Attribut Mapping",
"iam_bindpass": "Bind Passwort", "iam_bindpass": "Bind Passwort",
"iam_periodic_full_sync": "Vollsynchronisation", "iam_periodic_full_sync": "Vollsynchronisation",
@ -308,6 +314,7 @@
"quarantine_release_format_att": "Als Anhang", "quarantine_release_format_att": "Als Anhang",
"quarantine_release_format_raw": "Unverändertes Original", "quarantine_release_format_raw": "Unverändertes Original",
"quarantine_retention_size": "Rückhaltungen pro Mailbox:<br><small>0 bedeutet <b>inaktiv</b>.</small>", "quarantine_retention_size": "Rückhaltungen pro Mailbox:<br><small>0 bedeutet <b>inaktiv</b>.</small>",
"quicklink_text": "Quicklinks zu anderen Login-Seiten unter der Loginform ein- oder ausblenden",
"quota_notification_html": "Benachrichtigungs-E-Mail Inhalt:<br><small>Leer lassen, um Standard-Template wiederherzustellen.</small>", "quota_notification_html": "Benachrichtigungs-E-Mail Inhalt:<br><small>Leer lassen, um Standard-Template wiederherzustellen.</small>",
"quota_notification_sender": "Benachrichtigungs-E-Mail Absender", "quota_notification_sender": "Benachrichtigungs-E-Mail Absender",
"quota_notification_subject": "Benachrichtigungs-E-Mail Betreff", "quota_notification_subject": "Benachrichtigungs-E-Mail Betreff",
@ -387,6 +394,7 @@
"unchanged_if_empty": "Unverändert, wenn leer", "unchanged_if_empty": "Unverändert, wenn leer",
"upload": "Hochladen", "upload": "Hochladen",
"username": "Benutzername", "username": "Benutzername",
"user_quicklink": "Quicklink zur Benutzer-Loginseite ausblenden",
"validate_license_now": "GUID erneut verifizieren", "validate_license_now": "GUID erneut verifizieren",
"verify": "Verifizieren", "verify": "Verifizieren",
"yes": "&#10003;", "yes": "&#10003;",
@ -398,7 +406,8 @@
"allowed_methods": "Access-Control-Allow-Methods", "allowed_methods": "Access-Control-Allow-Methods",
"allowed_origins": "Access-Control-Allow-Origin", "allowed_origins": "Access-Control-Allow-Origin",
"logo_dark_label": "Invertiert für den Darkmode", "logo_dark_label": "Invertiert für den Darkmode",
"logo_normal_label": "Normal" "logo_normal_label": "Normal",
"user_link": "Nutzer-Link"
}, },
"danger": { "danger": {
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten", "access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
@ -806,9 +815,13 @@
"forgot_password": "> Passwort vergessen?", "forgot_password": "> Passwort vergessen?",
"invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.<br>Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.", "invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.<br>Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.",
"login": "Anmelden", "login": "Anmelden",
"login_user": "Benutzer Anmelden", "login_linkstext": "Nicht der richtige Login?",
"login_dadmin": "Domain-Administrator Anmelden", "login_usertext": "Als Benutzer anmelden",
"login_admin": "Administrator Anmelden", "login_domainadmintext": "Als Domainadmin anmelden",
"login_admintext": "Als Admin anmelden",
"login_user": "Anmeldung als Benutzer",
"login_dadmin": "Anmeldung als Domain-Administrator",
"login_admin": "Anmeldung als Administrator",
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
"new_password": "Neues Passwort", "new_password": "Neues Passwort",
"new_password_confirm": "Neues Passwort bestätigen", "new_password_confirm": "Neues Passwort bestätigen",
@ -991,7 +1004,8 @@
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit", "syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen" "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
"iam": "Identitätsanbieter"
}, },
"oauth2": { "oauth2": {
"access_denied": "Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.", "access_denied": "Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.",
@ -1094,6 +1108,7 @@
"bcc_edited": "BCC-Map-Eintrag %s wurde geändert", "bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
"bcc_saved": "BCC- Map-Eintrag wurde gespeichert", "bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
"cors_headers_edited": "CORS Einstellungen wurden erfolgreich gespeichert", "cors_headers_edited": "CORS Einstellungen wurden erfolgreich gespeichert",
"custom_login_modified": "Login Anpassung wurde erfolgreich gespeichert",
"db_init_complete": "Datenbankinitialisierung abgeschlossen", "db_init_complete": "Datenbankinitialisierung abgeschlossen",
"delete_filter": "Filter-ID %s wurde gelöscht", "delete_filter": "Filter-ID %s wurde gelöscht",
"delete_filters": "Filter gelöscht: %s", "delete_filters": "Filter gelöscht: %s",

View File

@ -134,6 +134,7 @@
"admin_domains": "Domain assignments", "admin_domains": "Domain assignments",
"admins": "Administrators", "admins": "Administrators",
"admins_ldap": "LDAP Administrators", "admins_ldap": "LDAP Administrators",
"admin_quicklink": "Hide Quicklink to Admin Login Page",
"advanced_settings": "Advanced settings", "advanced_settings": "Advanced settings",
"allowed_methods": "Access-Control-Allow-Methods", "allowed_methods": "Access-Control-Allow-Methods",
"allowed_origins": "Access-Control-Allow-Origin", "allowed_origins": "Access-Control-Allow-Origin",
@ -161,6 +162,7 @@
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.", "credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
"customer_id": "Customer ID", "customer_id": "Customer ID",
"customize": "Customize", "customize": "Customize",
"login_page": "Login Page",
"destination": "Destination", "destination": "Destination",
"dkim_add_key": "Add ARC/DKIM key", "dkim_add_key": "Add ARC/DKIM key",
"dkim_domains_selector": "Selector", "dkim_domains_selector": "Selector",
@ -179,6 +181,7 @@
"domain": "Domain", "domain": "Domain",
"domain_admin": "Domain administrator", "domain_admin": "Domain administrator",
"domain_admins": "Domain administrators", "domain_admins": "Domain administrators",
"domainadmin_quicklink": "Hide Quicklink to Domainadmin Login Page",
"domain_s": "Domain/s", "domain_s": "Domain/s",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"duplicate_dkim": "Duplicate DKIM record", "duplicate_dkim": "Duplicate DKIM record",
@ -202,6 +205,8 @@
"f2b_whitelist": "Whitelisted networks/hosts", "f2b_whitelist": "Whitelisted networks/hosts",
"filter": "Filter", "filter": "Filter",
"filter_table": "Filter table", "filter_table": "Filter table",
"force_sso_text": "If an external OIDC provider is configured, this option hides the default mailcow login froms and only shows the Singe Sign-On button",
"force_sso": "Disable mailcow Login and show only Singe Sign-On",
"forwarding_hosts": "Forwarding Hosts", "forwarding_hosts": "Forwarding Hosts",
"forwarding_hosts_add_hint": "You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).", "forwarding_hosts_add_hint": "You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).",
"forwarding_hosts_hint": "Incoming messages are unconditionally accepted from any hosts listed here. These hosts are then not checked against DNSBLs or subjected to greylisting. Spam received from them is never rejected, but optionally it can be filed into the Junk folder. The most common use for this is to specify mail servers on which you have set up a rule that forwards incoming emails to your mailcow server.", "forwarding_hosts_hint": "Incoming messages are unconditionally accepted from any hosts listed here. These hosts are then not checked against DNSBLs or subjected to greylisting. Spam received from them is never rejected, but optionally it can be filed into the Junk folder. The most common use for this is to specify mail servers on which you have set up a rule that forwards incoming emails to your mailcow server.",
@ -229,6 +234,7 @@
"iam_host": "Host", "iam_host": "Host",
"iam_host_info": "Enter one or more LDAP hosts, separated by commas.", "iam_host_info": "Enter one or more LDAP hosts, separated by commas.",
"iam_import_users": "Import Users", "iam_import_users": "Import Users",
"iam_login_provisioning": "Auto-create users on login",
"iam_mapping": "Attribute Mapping", "iam_mapping": "Attribute Mapping",
"iam_bindpass": "Bind Password", "iam_bindpass": "Bind Password",
"iam_periodic_full_sync": "Periodic Full Sync", "iam_periodic_full_sync": "Periodic Full Sync",
@ -317,6 +323,7 @@
"quarantine_release_format_att": "As attachment", "quarantine_release_format_att": "As attachment",
"quarantine_release_format_raw": "Unmodified original", "quarantine_release_format_raw": "Unmodified original",
"quarantine_retention_size": "Retentions per mailbox:<br><small>0 indicates <b>inactive</b>.</small>", "quarantine_retention_size": "Retentions per mailbox:<br><small>0 indicates <b>inactive</b>.</small>",
"quicklink_text": "Show or hide quick links to other login pages under the login form",
"quota_notification_html": "Notification email template:<br><small>Leave empty to restore default template.</small>", "quota_notification_html": "Notification email template:<br><small>Leave empty to restore default template.</small>",
"quota_notification_sender": "Notification email sender", "quota_notification_sender": "Notification email sender",
"quota_notification_subject": "Notification email subject", "quota_notification_subject": "Notification email subject",
@ -398,6 +405,7 @@
"upload": "Upload", "upload": "Upload",
"username": "Username", "username": "Username",
"user_link": "User-Link", "user_link": "User-Link",
"user_quicklink": "Hide Quicklink to User Login Page",
"validate_license_now": "Validate GUID against license server", "validate_license_now": "Validate GUID against license server",
"verify": "Verify", "verify": "Verify",
"yes": "&#10003;" "yes": "&#10003;"
@ -809,6 +817,10 @@
"forgot_password": "> Forgot Password?", "forgot_password": "> Forgot Password?",
"invalid_pass_reset_token": "The reset password token is invalid or has expired.<br>Please request a new password reset link.", "invalid_pass_reset_token": "The reset password token is invalid or has expired.<br>Please request a new password reset link.",
"login": "Login", "login": "Login",
"login_linkstext": "Not the correct login?",
"login_usertext": "Log in as user",
"login_domainadmintext": "Log in as domain admin",
"login_admintext": "Log in as admin",
"login_user": "User Login", "login_user": "User Login",
"login_dadmin": "Domain-Administrator Login", "login_dadmin": "Domain-Administrator Login",
"login_admin": "Administrator Login", "login_admin": "Administrator Login",
@ -1105,6 +1117,7 @@
"bcc_edited": "BCC map entry %s edited", "bcc_edited": "BCC map entry %s edited",
"bcc_saved": "BCC map entry saved", "bcc_saved": "BCC map entry saved",
"cors_headers_edited": "CORS settings have been saved", "cors_headers_edited": "CORS settings have been saved",
"custom_login_modified": "Login customisation was saved successfully",
"db_init_complete": "Database initialization completed", "db_init_complete": "Database initialization completed",
"delete_filter": "Deleted filters ID %s", "delete_filter": "Deleted filters ID %s",
"delete_filters": "Deleted filters: %s", "delete_filters": "Deleted filters: %s",

View File

@ -730,7 +730,10 @@
"new_password": "Nouveau mot de passe", "new_password": "Nouveau mot de passe",
"new_password_confirm": "Confirmer le nouveau mot de passe", "new_password_confirm": "Confirmer le nouveau mot de passe",
"reset_password": "Réinitialiser le mot de passe", "reset_password": "Réinitialiser le mot de passe",
"request_reset_password": "Demander le changement du mot de passe" "request_reset_password": "Demander le changement du mot de passe",
"login_user": "Connexion Utilisateur",
"login_dadmin": "Connexion Administrateur de domaine",
"login_admin": "Connexion Administrateur"
}, },
"mailbox": { "mailbox": {
"action": "Action", "action": "Action",

View File

@ -16,7 +16,16 @@
"login_as": "Pieteikšanās kā pastkastes lietotājam", "login_as": "Pieteikšanās kā pastkastes lietotājam",
"mailbox_relayhost": "Pasta kastītes relayhost maiņa", "mailbox_relayhost": "Pasta kastītes relayhost maiņa",
"prohibited": "Aizliegts ar ACL", "prohibited": "Aizliegts ar ACL",
"protocol_access": "Protokola piekļuves maiņa" "protocol_access": "Protokola piekļuves maiņa",
"pw_reset": "Ļaut atiestatīt mailcow lietotāja paroli",
"ratelimit": "Piekļuves biežuma ierobežojums",
"quarantine": "Karantīnas darbības",
"quarantine_attachments": "Karantīnas pielikumi",
"quarantine_category": "Mainīt karantīnas paziņojumu kategoriju",
"quarantine_notification": "Mainīt karantīnas paziņojumus",
"smtp_ip_access": "Mainīt SMTP atļautos saimniekdatorus",
"sogo_access": "Atļaut SOGo piekļuves pārvaldību",
"sogo_profile_reset": "Atiestatīt SOGo profilu"
}, },
"add": { "add": {
"activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.", "activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.",
@ -24,8 +33,8 @@
"add": "Pievienot", "add": "Pievienot",
"add_domain_only": "Tikai pievienot domēnu", "add_domain_only": "Tikai pievienot domēnu",
"add_domain_restart": "Pievienot domēnu un restartēt SOGo", "add_domain_restart": "Pievienot domēnu un restartēt SOGo",
"alias_address": "Aizstājaddrese/s", "alias_address": "Aizstājadrese/s",
"alias_address_info": "<small>Pilna epasta addrese/s vai @piemērs.com, lai notvertu visas domēna ziņas (komatu atdalītas). <b>tikai mailcow domēni</b>.</small>", "alias_address_info": "<small>Pilna epasta adrese/s vai @example.com, lai notvertu visus domēna ziņojumus (atdalītas ar komatu). <b>Tikai mailcow domēni</b>.</small>",
"alias_domain": "Aizstājdomēni", "alias_domain": "Aizstājdomēni",
"alias_domain_info": "<small>Tikai derīgi domēna vārdi (komatu atdalīti).</small>", "alias_domain_info": "<small>Tikai derīgi domēna vārdi (komatu atdalīti).</small>",
"automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" etc.)", "automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" etc.)",
@ -63,15 +72,17 @@
"skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)",
"syncjob": "Pievienot sinhronizācijas darbu", "syncjob": "Pievienot sinhronizācijas darbu",
"syncjob_hint": "Ņemiet vērā, ka parole ir jāuzglabā vienkāršā tekstā!", "syncjob_hint": "Ņemiet vērā, ka parole ir jāuzglabā vienkāršā tekstā!",
"target_address": "Iet uz adresēm", "target_address": "Mērķa adreses",
"target_address_info": "<small>Pilna epasta addrese/s (comma-separated).</small>", "target_address_info": "<small>Pilna epasta adrese/s (atdalītas ar komatu).</small>",
"target_domain": "Mērķa domēns", "target_domain": "Mērķa domēns",
"username": "Lietotājvārds", "username": "Lietotājvārds",
"validate": "Apstiprināt", "validate": "Apstiprināt",
"validation_success": "Apstiprināts veiksmīgi", "validation_success": "Apstiprināts veiksmīgi",
"bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.<br>Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit.", "bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.<br>Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit.",
"domain_matches_hostname": "Domēns %s atbilst saimniekdatora nosaukumam", "domain_matches_hostname": "Domēns %s atbilst saimniekdatora nosaukumam",
"disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)",
"app_password": "Pievienot lietotnes paroli",
"app_passwd_protocols": "Atļautie lietotnes paroles protokoli"
}, },
"admin": { "admin": {
"access": "Pieeja", "access": "Pieeja",
@ -114,7 +125,7 @@
"f2b_whitelist": "Baltā saraksta tīkls/hosts", "f2b_whitelist": "Baltā saraksta tīkls/hosts",
"filter_table": "Filtru tabula", "filter_table": "Filtru tabula",
"forwarding_hosts": "Hostu pārsūtīšana", "forwarding_hosts": "Hostu pārsūtīšana",
"forwarding_hosts_add_hint": "Var norādīt vai nu IPv4/IPv6 addreses, tīklu ar CIDR apzīmējumu, saimniekdatoru nosaukumus (kas tiks atrisināti IP adresēs) vai arī domēna vārdus (kas tiks atrisināti IP adresēs, vaicājot SPF ierakstus, vai, ja tādu nav, MX ierakstus).", "forwarding_hosts_add_hint": "Var norādīt vai nu IPv4/IPv6 adreses, tīklu ar CIDR apzīmējumu, saimniekdatoru nosaukumus (kas tiks atrisināti IP adresēs) vai arī domēna vārdus (kas tiks atrisināti IP adresēs, vaicājot SPF ierakstus, vai, ja tādu nav, MX ierakstus).",
"forwarding_hosts_hint": "Ienākošie ziņojumi tiek bez nosacījumiem pieņemti no visiem šeit norādītajiem saimniekdatoriem. Tie tad netiek pārbaudīti pret DNSBL vai pakļauti ievietošanai pelēkajā sarakstā. No tiem saņemtās mēstules nekad netiek noraidītas, bet pēc izvēles tās var pārvietot mapē \"Nevēlams\". Visbiežāk to izmanto, lai norādītu pasta serverus, kuros ir uzstādīts nosacījums, kas pārsūta ienākošās e-pasta vēstules uz Tavu mailcow serveri.", "forwarding_hosts_hint": "Ienākošie ziņojumi tiek bez nosacījumiem pieņemti no visiem šeit norādītajiem saimniekdatoriem. Tie tad netiek pārbaudīti pret DNSBL vai pakļauti ievietošanai pelēkajā sarakstā. No tiem saņemtās mēstules nekad netiek noraidītas, bet pēc izvēles tās var pārvietot mapē \"Nevēlams\". Visbiežāk to izmanto, lai norādītu pasta serverus, kuros ir uzstādīts nosacījums, kas pārsūta ienākošās e-pasta vēstules uz Tavu mailcow serveri.",
"help_text": "Pārrakstīt palīdzības tekstu zem pieteikšanās maskas (var izmantot HTML)", "help_text": "Pārrakstīt palīdzības tekstu zem pieteikšanās maskas (var izmantot HTML)",
"host": "Hosts", "host": "Hosts",
@ -142,7 +153,7 @@
"recipients": "Adresāts", "recipients": "Adresāts",
"refresh": "Atsvaidzināt", "refresh": "Atsvaidzināt",
"regen_api_key": "Reģenerēt API atslēgu", "regen_api_key": "Reģenerēt API atslēgu",
"relay_from": "\"No:\" addrese", "relay_from": "\"No:\" adrese",
"relay_run": "Palaist testu", "relay_run": "Palaist testu",
"relayhosts_hint": "Norādīt no sūtītāja atkarīgas piegādes, lai varētu tos atlasīt domēnu konfigurācijas uzvednē.<br>\n Piegādes pakalpojums vienmēr ir \"smtp\", tādējādi tiks mēģināts TLS, kad piedāvāts. Iekļautais TLS (SMTPS) netiek atbalstīts. Tiek ņemts vērā lietotāja atsevišķais izejošā TLS nosacījuma iestatījums.<br>\n Ietekmē atlasītos domēnus, tajā skaitā aizstājdomēnus.", "relayhosts_hint": "Norādīt no sūtītāja atkarīgas piegādes, lai varētu tos atlasīt domēnu konfigurācijas uzvednē.<br>\n Piegādes pakalpojums vienmēr ir \"smtp\", tādējādi tiks mēģināts TLS, kad piedāvāts. Iekļautais TLS (SMTPS) netiek atbalstīts. Tiek ņemts vērā lietotāja atsevišķais izejošā TLS nosacījuma iestatījums.<br>\n Ietekmē atlasītos domēnus, tajā skaitā aizstājdomēnus.",
"remove": "Noņemt", "remove": "Noņemt",
@ -222,7 +233,8 @@
"targetd_not_found": "Mērķa domēns nav atrasts", "targetd_not_found": "Mērķa domēns nav atrasts",
"username_invalid": "Lietotājvārds nevar tikt izmantots", "username_invalid": "Lietotājvārds nevar tikt izmantots",
"validity_missing": "Lūdzu piešķiriet derīguma termiņu", "validity_missing": "Lūdzu piešķiriet derīguma termiņu",
"domain_cannot_match_hostname": "Domēns nevar atbilst saimniekdatora nosaukumam" "domain_cannot_match_hostname": "Domēns nevar atbilst saimniekdatora nosaukumam",
"app_passwd_id_invalid": "Lietotnes paroles Id %s ir nederīgs"
}, },
"diagnostics": { "diagnostics": {
"cname_from_a": "Vērtība, kas iegūta no A/AAAA ieraksta. Tas tiek atbalstīts tik ilgi, kamēr ieraksts norāda uz pareizo resursu.", "cname_from_a": "Vērtība, kas iegūta no A/AAAA ieraksta. Tas tiek atbalstīts tik ilgi, kamēr ieraksts norāda uz pareizo resursu.",
@ -257,7 +269,7 @@
"hostname": "Saimniekdatora nosaukums", "hostname": "Saimniekdatora nosaukums",
"inactive": "Neaktīvs", "inactive": "Neaktīvs",
"kind": "Veids", "kind": "Veids",
"mailbox": "Rediģēt pastkasti", "mailbox": "Labot pastkasti",
"max_aliases": "Lielākais aizstājvārdu skaits", "max_aliases": "Lielākais aizstājvārdu skaits",
"max_mailboxes": "Maks. iespējamās pastkastes", "max_mailboxes": "Maks. iespējamās pastkastes",
"max_quota": "Maks. kvota uz pastkasti (MiB)", "max_quota": "Maks. kvota uz pastkasti (MiB)",
@ -283,8 +295,8 @@
"spam_policy": "Pievienot vai noņemt vienumus baltajā-/melnajā sarakstā", "spam_policy": "Pievienot vai noņemt vienumus baltajā-/melnajā sarakstā",
"spam_score": "Iestatīt pielāgotu surogātpasta vērtējumu", "spam_score": "Iestatīt pielāgotu surogātpasta vērtējumu",
"subfolder2": "Sinhronizēt galamērķa apakšmapē<br><small>(tukšs = neizmantot apakšmapi)</small>", "subfolder2": "Sinhronizēt galamērķa apakšmapē<br><small>(tukšs = neizmantot apakšmapi)</small>",
"syncjob": "Rediģēt sinhronizācijas darbu", "syncjob": "Labot sinhronizācijas darbu",
"target_address": "Iet uz adresi/ēm <small>(komatu atdalītas)</small>", "target_address": "Mērķa adrese/s <small>(atdalītas ar komatu)</small>",
"target_domain": "Mērķa domēns", "target_domain": "Mērķa domēns",
"title": "Labot priekšmetu", "title": "Labot priekšmetu",
"unchanged_if_empty": "Ja neizmainīts atstājiet tukšu", "unchanged_if_empty": "Ja neizmainīts atstājiet tukšu",
@ -300,8 +312,11 @@
"sogo_visible": "Aizstājvārds ir redzams SOGo", "sogo_visible": "Aizstājvārds ir redzams SOGo",
"sogo_visible_info": "Šī iespēja ietekmē tikai tos objektus, kurus var parādīt SOGo (koplietojamās vai nekoplietojamās aizstājadreses, kas norāda uz vismaz vienu vietējo pastkasti). Ja paslēpts, netiks parādīts SOGo kā atlasāms sūtītājs.", "sogo_visible_info": "Šī iespēja ietekmē tikai tos objektus, kurus var parādīt SOGo (koplietojamās vai nekoplietojamās aizstājadreses, kas norāda uz vismaz vienu vietējo pastkasti). Ja paslēpts, netiks parādīts SOGo kā atlasāms sūtītājs.",
"mbox_rl_info": "Šis pieprasījumu ierobežojums tiek piemērots SASL pieteikšanās vārdam, tas atbilst jebkurai \"from\" adresei, ko izmanto lietotājs, kurš ir pieteicies. Pastkastes pieprasījumu ierobežojums pārraksta domēna mēroga pieprasījumu ierobežojumu.", "mbox_rl_info": "Šis pieprasījumu ierobežojums tiek piemērots SASL pieteikšanās vārdam, tas atbilst jebkurai \"from\" adresei, ko izmanto lietotājs, kurš ir pieteicies. Pastkastes pieprasījumu ierobežojums pārraksta domēna mēroga pieprasījumu ierobežojumu.",
"sogo_access": "Nodrošināt tiešu pieteikšanās piekļuvi SOGo", "sogo_access": "Tieša pārvirzīšana uz SOGo",
"disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)",
"app_passwd_protocols": "Atļautie lietotnes paroles protokoli",
"allowed_protocols": "Atļautie protokoli tiešai lietotāja piekļuvei (neietekmē lietotnes paroles protokolus)",
"app_passwd": "Lietotnes parole"
}, },
"footer": { "footer": {
"cancel": "Atcelt", "cancel": "Atcelt",
@ -508,7 +523,9 @@
"verified_fido2_login": "Apliecināta FIDO2 pieteikšanās", "verified_fido2_login": "Apliecināta FIDO2 pieteikšanās",
"verified_webauthn_login": "Apliecināta WebAuthn pieteikšanās", "verified_webauthn_login": "Apliecināta WebAuthn pieteikšanās",
"verified_totp_login": "Apliecināta TOTP pieteikšanās", "verified_totp_login": "Apliecināta TOTP pieteikšanās",
"verified_yotp_login": "Apliecināta Yubico OTP pieteikšanās" "verified_yotp_login": "Apliecināta Yubico OTP pieteikšanās",
"app_passwd_removed": "Noņemta lietotnes parole ar Id %s",
"app_passwd_added": "Pievienota jauna lietotnes parole"
}, },
"tfa": { "tfa": {
"api_register": "%s izmanto Yubico Cloud API. Lūdzu iegūstiet API atslēgu priekš Jūsu atslēgas<a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>", "api_register": "%s izmanto Yubico Cloud API. Lūdzu iegūstiet API atslēgu priekš Jūsu atslēgas<a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
@ -523,7 +540,7 @@
"scan_qr_code": "Lūdzu, skenējiet šo kodu ar savu autentifikācijas lietojumprogrammu vai ievadiet kodu manuāli.", "scan_qr_code": "Lūdzu, skenējiet šo kodu ar savu autentifikācijas lietojumprogrammu vai ievadiet kodu manuāli.",
"select": "Lūdzu izvēlaties", "select": "Lūdzu izvēlaties",
"set_tfa": "Uzstādīt difi faktoru autentifik;acijas metodi", "set_tfa": "Uzstādīt difi faktoru autentifik;acijas metodi",
"tfa": "Divu faktoru autentifikācija", "tfa": "Divpakāpju pieteikšanās",
"totp": "Uz laiku bāzēta vienreizēja parole (Google Autentifikātors utt.)", "totp": "Uz laiku bāzēta vienreizēja parole (Google Autentifikātors utt.)",
"webauthn": "WebAuthn autentifikācija", "webauthn": "WebAuthn autentifikācija",
"waiting_usb_auth": "<i>Gaida USB ierīci...</i><br><br>Lūdzu, tagad nospiežiet pogu uz Jūsu WebAuthn USB ierīces.", "waiting_usb_auth": "<i>Gaida USB ierīci...</i><br><br>Lūdzu, tagad nospiežiet pogu uz Jūsu WebAuthn USB ierīces.",
@ -618,12 +635,18 @@
"apple_connection_profile_mailonly": "Šis savienojuma profils iekļauj IMAP un SMTP konfigurācijas parametrus Apple ierīcei.", "apple_connection_profile_mailonly": "Šis savienojuma profils iekļauj IMAP un SMTP konfigurācijas parametrus Apple ierīcei.",
"pushover_info": "Pašpiegādes paziņojumu iestatījumi attieksies uz visu tīro (ne surogātpasta) pastu, kas piegādāts uz <b>%s</b>, ieskaitot aizstājvārdus (kopīgotus, nekopīgotus, ar birkām).", "pushover_info": "Pašpiegādes paziņojumu iestatījumi attieksies uz visu tīro (ne surogātpasta) pastu, kas piegādāts uz <b>%s</b>, ieskaitot aizstājvārdus (kopīgotus, nekopīgotus, ar birkām).",
"app_hint": "Lietotņu paroles ir aizstājējparoles, lai pieteiktos IMAP, SMTP, CalDAV, CardDAV un EAS. Lietotājvārds paliek nemainīgs. SOGo tīmekļa pasts nav pieejams ar lietotņu parolēm.", "app_hint": "Lietotņu paroles ir aizstājējparoles, lai pieteiktos IMAP, SMTP, CalDAV, CardDAV un EAS. Lietotājvārds paliek nemainīgs. SOGo tīmekļa pasts nav pieejams ar lietotņu parolēm.",
"direct_protocol_access": "Šim pastkastes lietotājam ir <b> tieša, ārēja piekļuve</b> zemāk uzskaitītajiem protokoliem un lietotnēm. Šo iestatījumu pārrauga pārvaldītājs. Lietotņu paroles var izveidot, lai nodrošinātu piekļuvi atsevišķiem protokoliem un lietotnēm.<br>Poga \"Pieteikties tīmekļa pastā\" nodrošina vienotu pieteikšanos SOGo un vienmēr ir pieejama.", "direct_protocol_access": "Šim pastkastes lietotājam ir <b> tieša, ārēja piekļuve</b> zemāk uzskaitītajiem protokoliem un lietotnēm. Šo iestatījumu pārrauga pārvaldītājs. Lietotņu paroles var izveidot, lai nodrošinātu piekļuvi atsevišķiem protokoliem un lietotnēm.<br>Poga \"Tīmekļa pasts\" nodrošina vienotu pieteikšanos SOGo un vienmēr ir pieejama.",
"last_ui_login": "Pēdējā pieteikšanās saskarnē", "last_ui_login": "Pēdējā pieteikšanās saskarnē",
"login_history": "Pieteikšanās vēsture", "login_history": "Pieteikšanās vēsture",
"no_last_login": "Nav informācijas par pēdējām pieteikšanās saskarnē reizēm", "no_last_login": "Nav informācijas par pēdējām pieteikšanās saskarnē reizēm",
"open_webmail_sso": "Pieteikšanās tīmekļa pastā", "open_webmail_sso": "Tīmekļa pasts",
"last_mail_login": "Pēdējā pasta pieteikšanās" "last_mail_login": "Pēdējā pasta pieteikšanās",
"change_password_hint_app_passwords": "Kontā ir %d lietotņu paroles, kas netiks mainītas. Lai pārvaldītu tās, jādodas uz cilni \"Lietotņu paroles\".",
"with_app_password": "ar lietotnes paroli",
"apple_connection_profile_with_app_password": "Jauna lietotnes parole ir izveidota un pievienota profilam, lai ierīces iestatīšanas laikā nebūtu nepieciešams ievadīt paroli. Lūgums nekopīgot datni, jo tā nodrošina pilnu piekļuvi pastkastei.",
"tfa_info": "Divpakāpju autentificēšanās palīdz aizsargāt kontu.Ja tā ir iespējota, var būt nepieciešamas lietotņu paroles, lai pieteiktos lietotnēs vai pakalpojumos, kas nenodrošina divpakāpju autentificēšanos (piem., e-pasta klienti).",
"app_passwds": "Lietotņu paroles",
"create_app_passwd": "Izveidot lietotnes paroli"
}, },
"datatables": { "datatables": {
"paginate": { "paginate": {

View File

@ -108,7 +108,8 @@
"timeout2": "本地主机连接超时时间", "timeout2": "本地主机连接超时时间",
"username": "用户名", "username": "用户名",
"validate": "验证", "validate": "验证",
"validation_success": "验证成功" "validation_success": "验证成功",
"dry": "模拟同步Dry run"
}, },
"admin": { "admin": {
"access": "权限管理", "access": "权限管理",
@ -358,7 +359,43 @@
"ip_check_disabled": "IP 检查已禁用。你可透过以下路径启用<br> <strong>系统 > 配置 > 选项 > 页面自定义</strong>", "ip_check_disabled": "IP 检查已禁用。你可透过以下路径启用<br> <strong>系统 > 配置 > 选项 > 页面自定义</strong>",
"queue_unban": "解除封禁", "queue_unban": "解除封禁",
"allowed_methods": "访问控制允许方式", "allowed_methods": "访问控制允许方式",
"allowed_origins": "访问控制允许原" "allowed_origins": "访问控制允许原",
"iam": "身份识别提供者",
"iam_attribute_field": "Attribute 域",
"iam_authorize_url": "Authorization endpoint",
"iam_auth_flow": "认证流程",
"iam_basedn": "Base DN",
"iam_client_id": "客户端 ID",
"iam_client_secret": "客户端凭据",
"iam_client_scopes": "客户端 Scopes",
"iam_default_template": "默认模板",
"iam_default_template_description": "如果未为用户分配模板,则在创建邮箱时将使用默认模板,但在更新邮箱时不会使用默认模板。",
"iam_description": "配置外部认证提供者<br>如果已设置好属性映射,用户在首次登录时将会自动创建其 Mailbox。",
"iam_host": "Host",
"iam_host_info": "请输入一个或多个 LDAP 主机,使用英文逗号分隔。",
"iam_import_users": "导入用户",
"iam_mapping": "属性映射",
"iam_bindpass": "密码绑定Bind Password",
"iam_periodic_full_sync": "周期性全量同步",
"iam_port": "端口",
"iam_realm": "Realm",
"iam_redirect_url": "重定向 Url",
"iam_rest_flow": "Mailpassword 流程",
"iam_server_url": "服务器 Url",
"iam_sso": "单点登录SSO",
"iam_sync_interval": "同步/导入周期min",
"iam_test_connection": "测试连接",
"iam_token_url": "Token endpoint",
"iam_userinfo_url": "User info endpoint",
"iam_username_field": "Username 域",
"iam_binddn": "Bind DN",
"iam_use_ssl": "使用 SSL",
"iam_use_tls": "使用 TLS",
"iam_version": "版本",
"ignore_ssl_error": "忽略 SSL 错误",
"iam_auth_flow_info": "除了在单点登录SSO中使用的 Authorization Code 流程(在 Keycloak 中是标准流程之外mailcow 还支持使用 Credentials 的身份认证流程。Mailpassword 流程尝试通过 Keycloak 的 Admin REST API 验证用户凭据mailcow 会从 Keycloak 中的 <code>mailcow_password</code> 属性中获取哈希后的密码。",
"filter": "过滤",
"iam_extra_permission": "要使以下设置生效Keycloak 中的 mailcow 客户端需要一个 <code>服务账户Service account</code> 以及 <code>查看用户view-users</code> 的权限。"
}, },
"danger": { "danger": {
"access_denied": "访问被拒绝或者表单数据无效", "access_denied": "访问被拒绝或者表单数据无效",
@ -495,7 +532,11 @@
"webauthn_authenticator_failed": "找不到所选的 authenticator", "webauthn_authenticator_failed": "找不到所选的 authenticator",
"webauthn_publickey_failed": "没有为选定的身份验证器保存公钥", "webauthn_publickey_failed": "没有为选定的身份验证器保存公钥",
"webauthn_username_failed": "所选的 authenticator 属于另一个账户", "webauthn_username_failed": "所选的 authenticator 属于另一个账户",
"demo_mode_enabled": "演示模式已开启" "demo_mode_enabled": "演示模式已开启",
"generic_server_error": "服务器错误。请联系您的管理员。",
"authsource_in_use": "由于当前有一个或多个用户正在使用该身份提供者IDP因此无法更改或删除。",
"iam_test_connection": "连接失败",
"required_data_missing": "缺少需要的 %s 数据"
}, },
"debug": { "debug": {
"chart_this_server": "图表 (此服务器)", "chart_this_server": "图表 (此服务器)",
@ -744,7 +785,10 @@
"new_password_confirm": "确认新密码", "new_password_confirm": "确认新密码",
"reset_password": "重置密码", "reset_password": "重置密码",
"request_reset_password": "请求重置密码", "request_reset_password": "请求重置密码",
"invalid_pass_reset_token": "密码重置 token 无效或已过期。<br> 请重新获取新的密码重置链接。" "invalid_pass_reset_token": "密码重置 token 无效或已过期。<br> 请重新获取新的密码重置链接。",
"login_user": "用户登录",
"login_dadmin": "域管理员登录",
"login_admin": "管理员登录"
}, },
"mailbox": { "mailbox": {
"action": "操作", "action": "操作",
@ -919,7 +963,8 @@
"max_quota": "每个信箱的最大容量配额", "max_quota": "每个信箱的最大容量配额",
"relay_unknown": "转发未知信箱", "relay_unknown": "转发未知信箱",
"templates": "模板", "templates": "模板",
"template": "模板" "template": "模板",
"iam": "身份提供者IDP"
}, },
"oauth2": { "oauth2": {
"access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权。", "access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权。",
@ -950,7 +995,7 @@
"neutral_danger": "无危险等级", "neutral_danger": "无危险等级",
"notified": "已发送通知", "notified": "已发送通知",
"qhandler_success": "已成功向系统发送请求,现在你可以关闭这个窗口了。", "qhandler_success": "已成功向系统发送请求,现在你可以关闭这个窗口了。",
"qid": "Rspamd QID", "qid": "Rspamd 队列IDQID",
"qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人<em>不</em>会知道)。\r\n <br>\"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\r\n <br>请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。<br>黑名单中项目会被隔离系统排除。", "qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人<em>不</em>会知道)。\r\n <br>\"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\r\n <br>请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。<br>黑名单中项目会被隔离系统排除。",
"qitem": "隔离项目", "qitem": "隔离项目",
"quarantine": "隔离", "quarantine": "隔离",
@ -1097,7 +1142,8 @@
"recovery_email_sent": "重置邮件已发送至 %s", "recovery_email_sent": "重置邮件已发送至 %s",
"template_added": "新增了模板 %s", "template_added": "新增了模板 %s",
"template_modified": "模板 %s 的修改已保存", "template_modified": "模板 %s 的修改已保存",
"template_removed": "模板 ID %s 已删除" "template_removed": "模板 ID %s 已删除",
"iam_test_connection": "连接成功"
}, },
"tfa": { "tfa": {
"api_register": "%s 使用了 Yubico Cloud API请<a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">在此</a>为你的密钥获取 API 密钥", "api_register": "%s 使用了 Yubico Cloud API请<a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">在此</a>为你的密钥获取 API 密钥",
@ -1292,7 +1338,11 @@
"password_reset_info": "如果不提供密码重置邮箱,此功能将无法使用。", "password_reset_info": "如果不提供密码重置邮箱,此功能将无法使用。",
"pushover_sound": "声音", "pushover_sound": "声音",
"value": "值", "value": "值",
"attribute": "属性" "attribute": "属性",
"protocols": "协议",
"authentication": "认证",
"tfa_info": "两步验证有助于保护您的账户安全。启用后,对于不支持两步验证的应用程序或服务(例如邮件客户端),需要使用应用专用密码进行登录。",
"overview": "概览"
}, },
"warning": { "warning": {
"cannot_delete_self": "不能删除已登录的用户", "cannot_delete_self": "不能删除已登录的用户",

View File

@ -94,7 +94,8 @@ elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HT
!empty($email) && !empty($email) &&
filter_var($email, FILTER_VALIDATE_EMAIL) && filter_var($email, FILTER_VALIDATE_EMAIL) &&
is_array($_SESSION[$session_var_user_allowed]) && is_array($_SESSION[$session_var_user_allowed]) &&
in_array($email, $_SESSION[$session_var_user_allowed]) in_array($email, $_SESSION[$session_var_user_allowed]) &&
!$_SESSION['pending_pw_update']
) { ) {
$username = $email; $username = $email;
$password = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); $password = file_get_contents("/etc/sogo-sso/sogo-sso.pass");

View File

@ -51,7 +51,41 @@
</div></p> </div></p>
</form> </form>
</div> </div>
<legend>{{ lang.admin.app_links }}</legend><hr /> <legend style="padding-top:20px" unselectable="on">{{ lang.admin.login_page }}</legend><hr />
<div>
<form class="form" data-id="custom_login" role="form" method="post">
<p class="text-muted">{{ lang.admin.quicklink_text }}</p>
<div class="ms-2 mb-1">
<input class="form-check-input" type="checkbox" value="1" name="hide_user_quicklink" id="hide_user_quicklink" {% if custom_login.hide_user_quicklink == 1 %}checked{% endif %}>
<label class="form-check-label" for="hide_user_quicklink">
{{ lang.admin.user_quicklink|raw }}
</label>
</div>
<div class="ms-2 mb-1">
<input class="form-check-input" type="checkbox" value="1" name="hide_domainadmin_quicklink" id="hide_domainadmin_quicklink" {% if custom_login.hide_domainadmin_quicklink == 1 %}checked{% endif %}>
<label class="form-check-label" for="hide_domainadmin_quicklink">
{{ lang.admin.domainadmin_quicklink|raw }}
</label>
</div>
<div class="ms-2 mb-4">
<input class="form-check-input" type="checkbox" value="1" name="hide_admin_quicklink" id="hide_admin_quicklink" {% if custom_login.hide_admin_quicklink == 1 %}checked{% endif %}>
<label class="form-check-label" for="hide_admin_quicklink">
{{ lang.admin.admin_quicklink|raw }}
</label>
</div>
<p class="text-muted">{{ lang.admin.force_sso_text|raw }}</p>
<div class="ms-2 mb-4">
<input class="form-check-input" type="checkbox" value="1" name="force_sso" id="force_sso" {% if custom_login.force_sso == 1 %}checked{% endif %}>
<label class="form-check-label" for="force_sso">
{{ lang.admin.force_sso|raw }}
</label>
</div>
<p><div class="btn-group">
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" data-action="edit_selected" data-item="admin" data-id="custom_login" data-reload="no" data-api-url='edit/custom_login' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div></p>
</form>
</div>
<legend style="padding-top:20px">{{ lang.admin.app_links }}</legend><hr />
<p class="text-muted">{{ lang.admin.merged_vars_hint|raw }}</p> <p class="text-muted">{{ lang.admin.merged_vars_hint|raw }}</p>
<form class="form-inline" data-id="app_links" role="form" method="post"> <form class="form-inline" data-id="app_links" role="form" method="post">
<table class="table table-condensed" style="white-space: nowrap;" id="app_link_table"> <table class="table table-condensed" style="white-space: nowrap;" id="app_link_table">

View File

@ -110,7 +110,7 @@
<p> <p>
<span class="badge fs-7 bg-info d-block d-sm-inline-block"> <span class="badge fs-7 bg-info d-block d-sm-inline-block">
<i class="bi bi-funnel-fill"></i> <i class="bi bi-funnel-fill"></i>
<a href="https://bgp.he.net/ip/{{ active_ban.ip }}" target="_blank"> <a href="https://bgp.tools/prefix/{{ active_ban.ip }}" target="_blank">
{{ active_ban.network }} {{ active_ban.network }}
</a> </a>
({{ active_ban.banned_until }}) ({{ active_ban.banned_until }})
@ -130,7 +130,7 @@
<p> <p>
<span class="badge fs-7 bg-danger d-block d-sm-inline-block"> <span class="badge fs-7 bg-danger d-block d-sm-inline-block">
<i class="bi bi-funnel-fill"></i> <i class="bi bi-funnel-fill"></i>
<a href="https://bgp.he.net/ip/{{ perm_ban.ip }}" target="_blank"> <a href="https://bgp.tools/prefix/{{ perm_ban.ip }}" target="_blank">
{{ perm_ban.network }} {{ perm_ban.network }}
</a> </a>
</span> </span>

View File

@ -64,10 +64,42 @@
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end"> <div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label" for="iam_keycloak_redirecturl">{{ lang.admin.iam_redirect_url }}:</label> <label class="control-label">{{ lang.admin.iam_redirect_url }}:</label>
</div> </div>
<div class="col-12 col-md-9 col-lg-4"> <div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" id="iam_keycloak_redirecturl" name="redirect_url" value="{{ iam_settings.redirect_url }}" required> <div class="row px-2 align-items-center">
<span class="col-10 p-0 pe-2">
<input type="text" class="form-control" name="redirect_url" value="{{ iam_settings.redirect_url }}" required>
</span>
<div class="col-2 p-0 d-flex">
<button class="btn btn-sm d-block d-sm-inline btn-secondary ms-auto iam_redirect_add_keycloak"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
</div>
</div>
<div class="row mb-2" id="iam_keycloak_redirect_list">
<input type="hidden" name="redirect_url_extra" value="">
{% for key, url in iam_settings.redirect_url_extra %}
<div class="offset-md-3 col-12 col-md-9 col-lg-4 mb-2">
<div class="row px-2">
<div class="col-10 p-0 pe-2">
<input type="text" class="form-control me-2" name="redirect_url_extra" value="{{ iam_settings.redirect_url_extra[key] }}">
</div>
<div class="col-2 p-0 d-flex">
<button class="iam_keycloak_redirect_del btn btn-sm d-block d-sm-inline btn-secondary ms-auto"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
{% endfor %}
<div class="offset-md-3 col-12 col-md-9 col-lg-4 mb-2">
<div class="row px-2">
<div class="col-10 p-0 pe-2">
<input type="text" class="form-control me-2" name="redirect_url_extra" value="">
</div>
<div class="col-2 p-0 d-flex">
<button class="iam_keycloak_redirect_del btn btn-sm d-block d-sm-inline btn-secondary ms-auto"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
@ -187,6 +219,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label">{{ lang.admin.iam_login_provisioning }}</label>
</div>
<div class="col-12 col-md-9">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="login_provisioning" value="1" {% if iam_settings.login_provisioning == 1 %}checked{% endif %}>
</div>
</div>
</div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end"> <div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label">{{ lang.admin.iam_periodic_full_sync }}</label> <label class="control-label">{{ lang.admin.iam_periodic_full_sync }}</label>
@ -274,10 +316,42 @@
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end"> <div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label" for="iam_redirect_url">{{ lang.admin.iam_redirect_url }}:</label> <label class="control-label">{{ lang.admin.iam_redirect_url }}:</label>
</div> </div>
<div class="col-12 col-md-9 col-lg-4"> <div class="col-12 col-md-9 col-lg-4">
<input type="text" class="form-control" id="iam_redirect_url" name="redirect_url" value="{{ iam_settings.redirect_url }}" required> <div class="row px-2 align-items-center">
<span class="col-10 p-0 pe-2">
<input type="text" class="form-control" name="redirect_url" value="{{ iam_settings.redirect_url }}" required>
</span>
<div class="col-2 p-0 d-flex">
<button class="btn btn-sm d-block d-sm-inline btn-secondary ms-auto iam_redirect_add_generic"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
</div>
</div>
<div class="row mb-2" id="iam_generic_redirect_list">
<input type="hidden" name="redirect_url_extra" value="">
{% for key, url in iam_settings.redirect_url_extra %}
<div class="offset-md-3 col-12 col-md-9 col-lg-4 mb-2">
<div class="row px-2">
<div class="col-10 p-0 pe-2">
<input type="text" class="form-control me-2" name="redirect_url_extra" value="{{ iam_settings.redirect_url_extra[key] }}">
</div>
<div class="col-2 p-0 d-flex">
<button class="iam_generic_redirect_del btn btn-sm d-block d-sm-inline btn-secondary ms-auto"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
{% endfor %}
<div class="offset-md-3 col-12 col-md-9 col-lg-4 mb-2">
<div class="row px-2">
<div class="col-10 p-0 pe-2">
<input type="text" class="form-control me-2" name="redirect_url_extra" value="">
</div>
<div class="col-2 p-0 d-flex">
<button class="iam_generic_redirect_del btn btn-sm d-block d-sm-inline btn-secondary ms-auto"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
@ -366,7 +440,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end"> <div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label">{{ lang.admin.ignore_ssl_error }}</label> <label class="control-label">{{ lang.admin.ignore_ssl_error }}</label>
</div> </div>
@ -376,6 +450,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-4">
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label">{{ lang.admin.iam_login_provisioning }}</label>
</div>
<div class="col-12 col-md-9">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="login_provisioning" value="1" {% if iam_settings.login_provisioning == 1 %}checked{% endif %}>
</div>
</div>
</div>
<div class="row mt-4 mb-2"> <div class="row mt-4 mb-2">
<div class="offset-md-3 col-12 col-md-9 d-flex flex-wrap"> <div class="offset-md-3 col-12 col-md-9 d-flex flex-wrap">
<div class="btn-group mb-2"> <div class="btn-group mb-2">
@ -582,6 +666,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label">{{ lang.admin.iam_login_provisioning }}</label>
</div>
<div class="col-12 col-md-9">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="login_provisioning" value="1" {% if iam_settings.login_provisioning == 1 %}checked{% endif %}>
</div>
</div>
</div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-3 d-flex align-items-center justify-content-md-end"> <div class="col-md-3 d-flex align-items-center justify-content-md-end">
<label class="control-label">{{ lang.admin.iam_periodic_full_sync }}</label> <label class="control-label">{{ lang.admin.iam_periodic_full_sync }}</label>

View File

@ -5,13 +5,28 @@
{% block content %} {% block content %}
<div class="row mb-4" style="margin-top: 60px"> <div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto"> <div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <div class="card-header d-flex align-items-center text-break">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.login_admin }} <i class="bi bi-person-fill me-2"></i> {{ lang.login.login_admin }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center"> <div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label> <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle"> <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
</div> </div>
<div class="ms-4 d-grid d-sm-block">
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="text-secondary btn p-0 border-0 bg-transparent ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
</button>
<ul class="dropdown-menu ms-auto login">
{% for key, val in available_languages %}
<li>
<a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
<span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="text-center mailcow-logo mb-4"> <div class="text-center mailcow-logo mb-4">
@ -37,23 +52,10 @@
</div> </div>
</div> </div>
<div class="d-flex justify-content-between mt-4" style="position: relative"> <div class="d-flex justify-content-between mt-4" style="position: relative">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button> <div class="d-grid d-sm-block"> <button type="submit" class="btn btn-xs-lg btn-success w-100 mt-2 mx-auto" style="max-width: 400px;" value="Login">{{ lang.login.login }}</button>
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
</button>
<ul class="dropdown-menu ms-auto login">
{% for key, val in available_languages %}
<li>
<a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
<span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
</form> </form>
<div class="hr-title mt-5"><strong>{{ lang.login.other_logins }}</strong></div> <div class="hr-title"><strong>{{ lang.login.other_logins }}</strong></div>
<div class="d-flex flex-column align-items-center"> <div class="d-flex flex-column align-items-center">
<a class="btn btn-xs-lg btn-secondary w-100" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a> <a class="btn btn-xs-lg btn-secondary w-100" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a>
</div> </div>
@ -86,6 +88,15 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if custom_login.hide_user_quicklink != 1 or custom_login.hide_domainadmin_quicklink != 1 %}
<p class="text-center mt-3 text-muted" style="font-size: 0.9rem;">
{{ lang.login.login_linkstext }}<br>
{% if custom_login.hide_user_quicklink != 1 %}<a href="/">{{ lang.login.login_usertext }}</a>{% endif %}
{% if custom_login.hide_user_quicklink != 1 and custom_login.hide_domainadmin_quicklink != 1 %}|{% endif %}
{% if custom_login.hide_domainadmin_quicklink != 1 %}<a href="/domainadmin">{{ lang.login.login_domainadmintext }}</a>{% endif %}
</p>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,13 +5,28 @@
{% block content %} {% block content %}
<div class="row mb-4" style="margin-top: 60px"> <div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto"> <div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <div class="card-header d-flex align-items-center text-break">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.login_dadmin }} <i class="bi bi-person-fill me-2"></i> {{ lang.login.login_dadmin }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center"> <div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label> <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle"> <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
</div> </div>
<div class="ms-4 d-grid d-sm-block">
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="text-secondary btn p-0 border-0 bg-transparent ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
</button>
<ul class="dropdown-menu ms-auto login">
{% for key, val in available_languages %}
<li>
<a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
<span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="text-center mailcow-logo mb-4"> <div class="text-center mailcow-logo mb-4">
@ -37,23 +52,10 @@
</div> </div>
</div> </div>
<div class="d-flex justify-content-between mt-4" style="position: relative"> <div class="d-flex justify-content-between mt-4" style="position: relative">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button> <div class="d-grid d-sm-block"> <button type="submit" class="btn btn-xs-lg btn-success w-100 mt-2 mx-auto" style="max-width: 400px;" value="Login">{{ lang.login.login }}</button>
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
</button>
<ul class="dropdown-menu ms-auto login">
{% for key, val in available_languages %}
<li>
<a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
<span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div> </div>
</form> </form>
<div class="hr-title mt-5"><strong>{{ lang.login.other_logins }}</strong></div> <div class="hr-title"><strong>{{ lang.login.other_logins }}</strong></div>
<div class="d-flex flex-column align-items-center"> <div class="d-flex flex-column align-items-center">
<a class="btn btn-xs-lg btn-secondary w-100" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a> <a class="btn btn-xs-lg btn-secondary w-100" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a>
</div> </div>
@ -86,6 +88,15 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if custom_login.hide_user_quicklink != 1 or custom_login.hide_admin_quicklink != 1 %}
<p class="text-center mt-3 text-muted" style="font-size: 0.9rem;">
{{ lang.login.login_linkstext }}<br>
{% if custom_login.hide_user_quicklink != 1 %}<a href="/">{{ lang.login.login_usertext }}</a>{% endif %}
{% if custom_login.hide_user_quicklink != 1 and custom_login.hide_admin_quicklink != 1 %}|{% endif %}
{% if custom_login.hide_admin_quicklink != 1 %}<a href="/admin">{{ lang.login.login_admintext }}</a>{% endif %}
</p>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,13 +5,30 @@
{% block content %} {% block content %}
<div class="row mb-4" style="margin-top: 60px"> <div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto"> <div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <div class="card-header d-flex align-items-center text-break">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.login_user }} <i class="bi bi-person-fill me-2"></i> {{ lang.login.login_user }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center"> <div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label> <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle"> <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
</div> </div>
{% if not oauth2_request %}
<div class="ms-4 d-grid d-sm-block">
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="text-secondary btn p-0 border-0 bg-transparent ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
</button>
<ul class="dropdown-menu ms-auto login">
{% for key, val in available_languages %}
<li>
<a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
<span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="text-center mailcow-logo mb-4"> <div class="text-center mailcow-logo mb-4">
@ -25,6 +42,7 @@
{% if is_mobileconfig %} {% if is_mobileconfig %}
<div class="my-4 alert alert-info ">{{ lang.login.mobileconfig_info }}</div> <div class="my-4 alert alert-info ">{{ lang.login.mobileconfig_info }}</div>
{% endif %} {% endif %}
{% if custom_login.force_sso != 1 %}
<form method="post" autofill="off"> <form method="post" autofill="off">
<div class="d-flex mt-3"> <div class="d-flex mt-3">
<label class="visually-hidden" for="login_user">{{ lang.login.username }}</label> <label class="visually-hidden" for="login_user">{{ lang.login.username }}</label>
@ -40,35 +58,22 @@
<input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password"> <input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password">
</div> </div>
</div> </div>
<div class="d-flex justify-content-between mt-4" style="position: relative"> <div class="mt-2 text-muted" style="font-size: 0.9rem;">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
{% if not oauth2_request %}
<div class="d-grid d-sm-block">
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
</button>
<ul class="dropdown-menu ms-auto login">
{% for key, val in available_languages %}
<li>
<a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
<span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</form>
<div class="mt-3">
<a href="/reset-password">{{ lang.login.forgot_password }}</a> <a href="/reset-password">{{ lang.login.forgot_password }}</a>
</div> </div>
<div class="hr-title mt-5"><strong>{{ lang.login.other_logins }}</strong></div> <div class="d-flex justify-content-between mt-4" style="position: relative">
<button type="submit" class="btn btn-xs-lg btn-success w-100 mt-2 mx-auto" style="max-width: 400px;" value="Login">{{ lang.login.login }}</button>
</div>
</form>
<div class="hr-title"><strong>{{ lang.login.other_logins }}</strong></div>
{% endif %}
<div class="d-flex flex-column align-items-center"> <div class="d-flex flex-column align-items-center">
{% if has_iam_sso %} {% if has_iam_sso %}
<a class="btn btn-xs-lg btn-secondary w-100 mt-2" style="max-width: 400px;" href="/?iam_sso=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.iam_sso }}</a> <a class="btn btn-xs-lg btn-secondary w-100 mt-2" style="max-width: 400px;" href="/?iam_sso=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.iam_sso }}</a>
{% endif %} {% endif %}
{% if custom_login.force_sso != 1 %}
<a class="btn btn-xs-lg btn-secondary w-100 mt-2" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a> <a class="btn btn-xs-lg btn-secondary w-100 mt-2" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a>
{% endif %}
</div> </div>
{% if login_delay %} {% if login_delay %}
<p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p> <p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p>
@ -96,9 +101,20 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </div>
<div>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if custom_login.hide_admin_quicklink != 1 or custom_login.hide_domainadmin_quicklink != 1 %}
<p class="text-center mt-3 text-muted" style="font-size: 0.9rem;">
{{ lang.login.login_linkstext }}<br>
{% if custom_login.hide_admin_quicklink != 1 %}<a href="/admin">{{ lang.login.login_admintext }}</a>{% endif %}
{% if custom_login.hide_admin_quicklink != 1 and custom_login.hide_domainadmin_quicklink != 1 %}|{% endif %}
{% if custom_login.hide_domainadmin_quicklink != 1 %}<a href="/domainadmin">{{ lang.login.login_domainadmintext }}</a>{% endif %}
</p>
{% endif %}
</div> </div>
</div> </div>
{% if not oauth2_request and ui_texts.help_text %} {% if not oauth2_request and ui_texts.help_text %}

View File

@ -84,7 +84,7 @@ services:
- clamd - clamd
rspamd-mailcow: rspamd-mailcow:
image: ghcr.io/mailcow/rspamd:2.1 image: ghcr.io/mailcow/rspamd:2.2
stop_grace_period: 30s stop_grace_period: 30s
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow
@ -176,6 +176,7 @@ services:
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
- SKIP_FTS=${SKIP_FTS:-y} - SKIP_FTS=${SKIP_FTS:-y}
- SKIP_CLAMD=${SKIP_CLAMD:-n} - SKIP_CLAMD=${SKIP_CLAMD:-n}
- SKIP_OLEFY=${SKIP_OLEFY:-n}
- SKIP_SOGO=${SKIP_SOGO:-n} - SKIP_SOGO=${SKIP_SOGO:-n}
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
- MASTER=${MASTER:-y} - MASTER=${MASTER:-y}
@ -497,7 +498,7 @@ services:
- /lib/modules:/lib/modules:ro - /lib/modules:/lib/modules:ro
watchdog-mailcow: watchdog-mailcow:
image: ghcr.io/mailcow/watchdog:2.07 image: ghcr.io/mailcow/watchdog:2.08
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
tmpfs: tmpfs:
@ -538,6 +539,7 @@ services:
- IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0} - IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0}
- CHECK_UNBOUND=${CHECK_UNBOUND:-1} - CHECK_UNBOUND=${CHECK_UNBOUND:-1}
- SKIP_CLAMD=${SKIP_CLAMD:-n} - SKIP_CLAMD=${SKIP_CLAMD:-n}
- SKIP_OLEFY=${SKIP_OLEFY:-n}
- SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
- SKIP_SOGO=${SKIP_SOGO:-n} - SKIP_SOGO=${SKIP_SOGO:-n}
- HTTPS_PORT=${HTTPS_PORT:-443} - HTTPS_PORT=${HTTPS_PORT:-443}
@ -589,7 +591,7 @@ services:
- dockerapi - dockerapi
olefy-mailcow: olefy-mailcow:
image: ghcr.io/mailcow/olefy:1.14 image: ghcr.io/mailcow/olefy:1.15
restart: always restart: always
environment: environment:
- TZ=${TZ} - TZ=${TZ}
@ -601,6 +603,7 @@ services:
- OLEFY_LOGLVL=20 - OLEFY_LOGLVL=20
- OLEFY_MINLENGTH=500 - OLEFY_MINLENGTH=500
- OLEFY_DEL_TMP=1 - OLEFY_DEL_TMP=1
- SKIP_OLEFY=${SKIP_OLEFY:-n}
networks: networks:
mailcow-network: mailcow-network:
aliases: aliases:

View File

@ -382,6 +382,10 @@ SKIP_UNBOUND_HEALTHCHECK=n
SKIP_CLAMD=${SKIP_CLAMD} SKIP_CLAMD=${SKIP_CLAMD}
# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n
SKIP_OLEFY=n
# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n # Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n
SKIP_SOGO=n SKIP_SOGO=n

View File

@ -323,6 +323,7 @@ adapt_new_options() {
"WATCHDOG_EXTERNAL_CHECKS" "WATCHDOG_EXTERNAL_CHECKS"
"WATCHDOG_SUBJECT" "WATCHDOG_SUBJECT"
"SKIP_CLAMD" "SKIP_CLAMD"
"SKIP_OLEFY"
"SKIP_IP_CHECK" "SKIP_IP_CHECK"
"ADDITIONAL_SAN" "ADDITIONAL_SAN"
"DOVEADM_PORT" "DOVEADM_PORT"
@ -967,6 +968,7 @@ CONFIG_ARRAY=(
"WATCHDOG_EXTERNAL_CHECKS" "WATCHDOG_EXTERNAL_CHECKS"
"WATCHDOG_SUBJECT" "WATCHDOG_SUBJECT"
"SKIP_CLAMD" "SKIP_CLAMD"
"SKIP_OLEFY"
"SKIP_IP_CHECK" "SKIP_IP_CHECK"
"ADDITIONAL_SAN" "ADDITIONAL_SAN"
"AUTODISCOVER_SAN" "AUTODISCOVER_SAN"
@ -1278,6 +1280,18 @@ for option in "${CONFIG_ARRAY[@]}"; do
echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf
echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf
fi fi
elif [[ "${option}" == "SKIP_CLAMD" ]]; then
if ! grep -q "${option}" mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf
echo 'SKIP_CLAMD=n' >> mailcow.conf
fi
elif [[ "${option}" == "SKIP_OLEFY" ]]; then
if ! grep -q "${option}" mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf
echo 'SKIP_OLEFY=n' >> mailcow.conf
fi
elif [[ "${option}" == "REDISPASS" ]]; then elif [[ "${option}" == "REDISPASS" ]]; then
if ! grep -q "${option}" mailcow.conf; then if ! grep -q "${option}" mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf" echo "Adding new option \"${option}\" to mailcow.conf"