Compare commits

...
46 Commits
Author SHA1 Message Date
FreddleSpl0it 04c9413ed9 [Rspamd] Remove the URLhaus and MalwareBazaar maps retired by abuse.ch 2026-11-10 13:50:48 +01:00
FreddleSpl0it 5e98d8e279 Merge pull request #7581 from peterschwps/fix/trim-logs-redis-password
[Dovecot] Mask Redis password in trim_logs output
2026-11-10 13:43:36 +01:00
FreddleSpl0it 4caecf77e0 Merge pull request #7610 from woodfairy/fix/brotli-cve-2026-111497
[Nginx][Clamd][Postfix] Rebuild images for brotli CVE-2026-111497 (CISA KEV)
2026-11-10 13:40:04 +01:00
FreddleSpl0it 37d304d075 Merge pull request #7544 from jorgeavlobo/fix/keycloak-admin-token-introspect
[Web] Keycloak: skip introspection of an expired admin token and reuse the new one
2026-11-10 01:45:16 +01:00
FreddleSpl0it 25086a9b3f Merge pull request #7488 from apio-sys/fix/rspamd-dqs-dbl-full-rule
[Rspamd] write full DBL rule when SPAMHAUS_DQS_KEY is set
2026-11-10 01:42:29 +01:00
FreddleSpl0it 7a3f1cbd41 Merge pull request #7530 from ThxGiving/fix/eas-auth-challenge
[Nginx] Send Basic auth challenge for unauthenticated ActiveSync requests
2026-11-10 01:39:28 +01:00
FreddleSpl0it 616717bc0d Merge pull request #7639 from mailcow/fix/7580
[Rspamd] Remove MX check overrides that rspamd ignores
2026-11-10 01:28:42 +01:00
FreddleSpl0it 6fd271ee64 [Rspamd] Remove MX check overrides that rspamd ignores 2026-11-10 01:27:52 +01:00
FreddleSpl0it 7459cc046a Merge pull request #7638 from mailcow/fix/7579
[Web] Template lists: sort size columns numerically
2026-11-10 01:08:36 +01:00
FreddleSpl0it 58dad73eee [Web] Template lists: sort size columns by bytes 2026-11-10 01:07:32 +01:00
FreddleSpl0it b21acd4787 Merge pull request #7637 from mailcow/fix/7557
[Web] Domain admins saw sender aliases of other domains
2026-11-10 01:02:39 +01:00
FreddleSpl0it 4ab954dcd1 [Web] Domain admins: show sender ACL and alias entries of their own domains only 2026-11-10 01:01:22 +01:00
FreddleSpl0it eefc17274e Merge pull request #7636 from mailcow/fix/7545
[Rspamd] WHITELISTED_FWD_HOST was skipped at the reject threshold
2026-11-10 00:57:45 +01:00
FreddleSpl0it 4878840bdd [Rspamd] Forwarding hosts: check them as prefilter and scan the mail completely 2026-11-10 00:56:53 +01:00
FreddleSpl0it 869bcb3c42 Merge pull request #7635 from mailcow/fix/7518
[Rspamd] Domain footer broke DKIM for mail without Content-Transfer-Encoding
2026-11-09 23:36:42 +01:00
FreddleSpl0it 0bcde13500 [Rspamd] Domain footer: add the Content-Transfer-Encoding header to the delivered mail 2026-11-09 23:34:57 +01:00
FreddleSpl0it 867b9dddab Merge pull request #7634 from mailcow/fix/7503
[Web] Keycloak: accept Base64-encoded mailcow_password
2026-11-09 23:30:20 +01:00
FreddleSpl0it a4326e6642 [Web] Keycloak Mailpassword Flow: accept Base64-encoded password hashes 2026-11-09 23:28:53 +01:00
FreddleSpl0it c5e89a9e03 Merge pull request #7633 from mailcow/fix/7501
[Web] Forwarding hosts: store IPv6 addresses in canonical form
2026-11-09 23:25:40 +01:00
FreddleSpl0it 45d31b3279 [Web] Forwarding hosts: store IPv6 addresses in canonical form 2026-11-09 23:24:39 +01:00
FreddleSpl0it 24780ea5fb Merge pull request #7632 from mailcow/fix/7489
[Rspamd] Quarantine stored nothing without Q_EXCLUDE_DOMAINS
2026-11-09 23:22:11 +01:00
FreddleSpl0it 38b031e10c [Rspamd] Quarantine: store mail when Q_EXCLUDE_DOMAINS is not set 2026-11-09 23:21:14 +01:00
FreddleSpl0it e2fb833ceb Merge pull request #7631 from mailcow/fix/7482
[Web] Autodiscover: no ActiveSync when autodiscoverType is imap
2026-11-09 23:18:50 +01:00
FreddleSpl0it e27cb83326 [Web] Autodiscover: no ActiveSync when autodiscoverType is imap 2026-11-09 23:17:55 +01:00
FreddleSpl0it f810cecd3a Merge pull request #7630 from mailcow/fix/7476
[Web] force_tfa did not prevent removing 2FA
2026-11-09 23:14:38 +01:00
FreddleSpl0it cfad223824 [Web] force_tfa: read the flag with JSON_VALUE 2026-11-09 23:13:26 +01:00
FreddleSpl0it 6d20463acb Merge pull request #7629 from mailcow/fix/7454
[Nginx] Edit and quarantine links: allow a trailing slash
2026-11-09 23:09:05 +01:00
FreddleSpl0it 1a42c19050 [Nginx] Accept a trailing slash in /edit and /qhandler links 2026-11-09 23:08:14 +01:00
FreddleSpl0it b9f8eb4c31 Merge pull request #7628 from mailcow/fix/7449
[Postfix-TLSPol] Update to 1.14.5
2026-11-09 23:05:35 +01:00
FreddleSpl0it ce4150f921 [Postfix-TLSPol] Update to 1.14.5 2026-11-09 23:04:33 +01:00
FreddleSpl0it 7cc8cac705 Merge pull request #7627 from mailcow/fix/7318
[Rspamd] Mails between 50 and 100 MiB were deferred forever
2026-11-09 23:01:43 +01:00
FreddleSpl0it 3fd1d017cf [Rspamd] Raise max_message above the Postfix message size limit 2026-11-09 23:00:38 +01:00
FreddleSpl0it 8597954f91 Merge pull request #7626 from mailcow/fix/7179
[Rspamd] Sub-addressing: read the tag from the original SMTP recipient
2026-11-09 22:55:05 +01:00
FreddleSpl0it 2b4ffe8808 [Rspamd] Sub-addressing: read the tag from the original SMTP recipient 2026-11-09 22:53:07 +01:00
FreddleSpl0it 1911847c3b Merge pull request #7625 from mailcow/fix/6931
[Web] Update "Last modified" when tags of a domain or mailbox change
2026-11-09 22:48:21 +01:00
FreddleSpl0it 497872080b [Web] Update "Last modified" when tags of a domain or mailbox change 2026-11-09 22:46:15 +01:00
FreddleSpl0it a0dc3447f8 Merge pull request #7624 from mailcow/fix/6853
[Web][Dovecot] Mailbox list and API: show the last SOGo login
2026-11-09 22:42:27 +01:00
FreddleSpl0it e0640949a2 [Web][Dovecot] Show the last SOGo login in the mailbox list and API 2026-11-09 22:39:42 +01:00
milkmaker bf744413bd Translations update from Weblate (#7623)
* [Web] Language file updated by 'Cleanup translation files' addon

Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Language file updated by 'Cleanup translation files' addon

Co-authored-by: milkmaker <milkmaker@mailcow.de>
2026-11-09 21:41:36 +01:00
FreddleSpl0it 262de81da1 Merge pull request #7621 from mailcow/fix/7606
[Dockerapi] Group IMAP grants: set ACLs only on existing folders
2026-11-09 19:43:38 +01:00
FreddleSpl0it 5f13bb2046 [Dockerapi] Group IMAP grants: set ACLs only on existing folders 2026-11-09 19:41:17 +01:00
woodfairy 0a0686ef19 [Nginx][Clamd][Postfix] Rebuild images for brotli CVE-2026-111497
The current nginx, clamd and postfix images ship a brotli decoder in the
affected range 1.0.3 - 1.2.0 (CVE-2026-111497, CVSS 9.8, listed in CISA KEV):

- nginx:1.30.6-2   brotli-libs 1.2.0-r1 (fixed in Alpine 3.24: 1.2.1-r0)
- clamd:1.4.6-1    brotli-libs 1.2.0-r1 (fixed in Alpine 3.24: 1.2.1-r0)
- postfix:3.10.12-1 libbrotli1 1.1.0-2+b7 (fixed in trixie-security:
  1.1.0-2+deb13u1, DSA-6582-1)

All other mailcow images already contain a fixed version.

clamd and postfix only need a rebuild, which pulls the fixed packages.
The postfix rebuild also picks up Debian's postfix 3.10.13, hence the
new tag postfix:3.10.13-1. The
nginx image never upgraded the packages of its base image, so it only got
fixes when nginx:<version>-alpine was republished. It now runs
apk upgrade like clamd and acme do.
2026-11-08 00:49:55 +01:00
Peter Schwips 24b91f6089 [Dovecot] Mask Redis password in trim_logs output 2026-10-30 23:54:21 +01:00
Jorge Lobo 2f76230daf [Web] Keycloak: skip introspection of expired admin token 2026-10-23 00:52:03 +01:00
Sebastian Müller 8db3cae826 [Nginx] Send Basic auth challenge for unauthenticated ActiveSync requests
Outlook for iOS/Android connects through Microsoft's sync service, which
sends each EAS command without credentials first and only retries with
Basic auth after a 401 + WWW-Authenticate challenge. SOGo answers these
unauthenticated POSTs (Provision, FolderSync, Sync, ...) with 403, so the
Outlook apps never get past Provision and show an empty mailbox (#5846).

Answer requests without an Authorization header with a Basic challenge
in nginx before they reach SOGo. Clients that send credentials up front
are not affected.
2026-10-20 20:48:30 +02:00
Joris Le BlanschandClaude Opus 5.5 9f934af3f2 [Rspamd] write full DBL rule when SPAMHAUS_DQS_KEY is set
The generated "DBL" block only set rbl and disable_monitoring, assuming
it would merge into the stock rule. It is included with override=true,
so it replaced the stock rule instead: DBL lost all its checks (urls,
emails, dkim, helo, rdns, replyto), no_ip, selectors and returncodes,
and rspamd fell back to querying the client IP against DBL.

Write the full stock rule pointed at the DQS zone, like the ZRD block.

Fixes #7485

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:20:23 +02:00
26 changed files with 214 additions and 90 deletions

No files matched your search

@@ -586,8 +586,8 @@ class DockerApi:
if not user or id in ('user=', 'group='):
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'user and id required' }, indent=4), media_type="application/json")
action = 'doveadm acl set -u "$ACL_USER" -- "$folder" "$ACL_ID" $ACL_RIGHTS' if rights else 'doveadm acl delete -u "$ACL_USER" -- "$folder" "$ACL_ID"'
script = ('set -o pipefail; doveadm mailbox list -u "$ACL_USER" | while IFS= read -r folder; do '
'case "$folder" in Shared/*) continue;; esac; ' + action + ' || exit 1; done && doveadm acl recalc -u "$ACL_USER"')
script = ('set -o pipefail; doveadm -f tab mailbox status -u "$ACL_USER" guid "*" | tail -n +2 | cut -f1 | while IFS= read -r folder; do '
'case "$folder" in Shared|Shared/*) continue;; esac; ' + action + ' || exit 1; done && doveadm acl recalc -u "$ACL_USER"')
acl_return = container.exec_run(["/bin/bash", "-c", script],
environment={ 'ACL_USER': user, 'ACL_ID': id, 'ACL_RIGHTS': ' '.join(rights) })
return self.exec_run_handler('generic', acl_return)
+1 -1
View File
@@ -4,7 +4,7 @@ catch_non_zero() {
${CMD} > /dev/null
EC=$?
if [ ${EC} -ne 0 ]; then
echo "Command ${CMD} failed to execute, exit code was ${EC}"
echo "Command ${CMD//${REDISPASS}/***} failed to execute, exit code was ${EC}"
fi
}
source /source_env.sh
+2 -1
View File
@@ -3,7 +3,8 @@ LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apk add --no-cache nginx \
RUN apk upgrade --no-cache && \
apk add --no-cache nginx \
python3 \
py3-pip && \
pip install --upgrade pip && \
+1 -1
View File
@@ -4,7 +4,7 @@ WORKDIR /src
ENV CGO_ENABLED=0 \
GO111MODULE=on \
NOOPT=1 \
VERSION=1.14.0
VERSION=1.14.5
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
cd /src/postfix-tlspol && \
+26 -1
View File
@@ -277,9 +277,34 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then
}
}
"DBL" {
# override the defaults for DBL defined in modules.d/rbl.conf
# full copy of the DBL rule from modules.d/rbl.conf, pointed at DQS:
# the override include replaces the stock rule instead of merging into it
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
disable_monitoring = true;
checks = ["emails", "dkim", "helo", "rdns", "replyto", "urls", "content_urls"];
no_ip = true;
ignore_defaults = true;
exclude_users = false;
emails_domainonly = true;
selector = {
from_smtp = "from('smtp'):domain";
from_mime = "from('mime'):domain";
mid = "header(Message-Id).regexp('@([^\.]+\.[^>]+)').last";
}
returncodes = {
DBL_SPAM = "127.0.1.2";
DBL_PHISH = "127.0.1.4";
DBL_MALWARE = "127.0.1.5";
DBL_BOTNET = "127.0.1.6";
DBL_ABUSE = "127.0.1.102";
DBL_ABUSE_REDIR = "127.0.1.103";
DBL_ABUSE_PHISH = "127.0.1.104";
DBL_ABUSE_MALWARE = "127.0.1.105";
DBL_ABUSE_BOTNET = "127.0.1.106";
DBL_PROHIBIT = "127.0.1.255";
DBL_BLOCKED_OPENRESOLVER = "127.255.255.254";
DBL_BLOCKED = "127.255.255.255";
}
}
"ZRD" {
ignore_defaults = true;
+2 -2
View File
@@ -116,8 +116,8 @@ if ($result === false){
exit;
}
if ($result) {
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $client_ip);
set_sasl_log($post['username'], $client_ip, $post['service']);
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $service . " from IP " . $client_ip);
set_sasl_log($post['username'], $client_ip, $service);
}
}
@@ -94,11 +94,11 @@ location / {
}
location /qhandler {
rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2;
rewrite ^/qhandler/([^/]+)/(.+?)/?$ /qhandler.php?action=$1&hash=$2;
}
location /edit {
rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2;
rewrite ^/edit/([^/]+)/(.+?)/?$ /edit.php?$1=$2;
}
location ~ ^/api/v1/(.*)$ {
@@ -187,6 +187,12 @@ location /sogo-auth-verify {
}
location ^~ /Microsoft-Server-ActiveSync {
# Outlook for iOS/Android (Microsoft's sync service) sends EAS commands without credentials
# and only authenticates after a 401 Basic challenge. SOGo answers those with 403, so challenge here.
if ($http_authorization = "") {
add_header WWW-Authenticate 'Basic realm="SOGo"' always;
return 401;
}
auth_request /sogo-auth-verify;
auth_request_set $user $upstream_http_x_user;
auth_request_set $auth $upstream_http_x_auth;
-4
View File
@@ -1,7 +1,3 @@
MX_IMPLICIT {
expression = "MX_GOOD & MX_MISSING";
score = -0.01;
}
VIRUS_FOUND {
expression = "CLAM_VIRUS & !MAILCOW_WHITE";
score = 2000.0;
-18
View File
@@ -23,24 +23,6 @@ symbols {
}
}
group "MX" {
"MX_INVALID" {
score = 0.5;
description = "No connectable MX";
one_shot = true;
}
"MX_MISSING" {
score = 2.0;
description = "No MX record";
one_shot = true;
}
"MX_GOOD" {
score = -0.01;
description = "MX was ok";
one_shot = true;
}
}
group "reputation" {
symbols = {
"IP_REPUTATION_HAM" {
-20
View File
@@ -5,12 +5,6 @@ RCPT_MAILCOW_DOMAIN {
symbols_set = ["RCPT_MAILCOW_DOMAIN"];
}
WHITELISTED_FWD_HOST {
type = "ip";
map = "redis://WHITELISTED_FWD_HOST";
symbols_set = ["WHITELISTED_FWD_HOST"];
}
BULK_HEADER {
type = "content";
map = "${LOCAL_CONFDIR}/custom/bulk_header.map";
@@ -151,20 +145,6 @@ BAD_LANG {
score = 5.0;
}
BAZAAR_ABUSE_CH {
type = "selector";
selector = "attachments(hex,md5)";
map = "https://bazaar.abuse.ch/export/txt/md5/recent/";
score = 10.0;
}
URLHAUS_ABUSE_CH {
type = "selector";
selector = "urls";
map = "https://urlhaus.abuse.ch/downloads/text_online/";
score = 10.0;
}
SMTP_LIMITED_ACCESS {
type = "user";
map = "redis://SMTP_LIMITED_ACCESS";
-1
View File
@@ -1,6 +1,5 @@
timeout = 8.0;
symbol_bad_mx = "MX_INVALID";
symbol_no_mx = "MX_MISSING";
symbol_good_mx = "MX_GOOD";
expire = 86400;
key_prefix = "rmx";
+2
View File
@@ -9,3 +9,5 @@ disable_monitoring = true;
# instead of silently accepting the message without further processing.
soft_reject_on_timeout = true;
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
# Must not be lower than message_size_limit in postfix main.cf
max_message = 134217728;
+63 -6
View File
@@ -362,6 +362,61 @@ rspamd_config:register_symbol({
priority = 19
})
rspamd_config:register_symbol({
name = 'WHITELISTED_FWD_HOST',
type = 'prefilter',
score = 0.0,
callback = function(task)
local rspamd_logger = require "rspamd_logger"
local redis_params = rspamd_parse_redis_server('fwd_host')
local ip = task:get_from_ip()
if ip == nil or not ip:is_valid() then
return false
end
local from_ip_string = tostring(ip)
local ip_check_table = {'WHITELISTED_FWD_HOST', from_ip_string}
local maxbits = 128
local minbits = 7
if ip:get_version() == 4 then
maxbits = 32
minbits = 8
end
for i=maxbits,minbits,-1 do
table.insert(ip_check_table, ip:apply_mask(i):to_string() .. "/" .. i)
end
local function fwd_host_cb(err, data)
if err or type(data) ~= 'table' then
rspamd_logger.infox(rspamd_config, "fwd_host query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
return
end
for _,v in pairs(data) do
if type(v) == 'string' then
task:insert_result('WHITELISTED_FWD_HOST', 1.0, from_ip_string)
task:set_flag('pass_all')
return
end
end
end
local redis_ret = rspamd_redis_make_request(task,
redis_params, -- connect params
'WHITELISTED_FWD_HOST', -- hash key
false, -- is write
fwd_host_cb, --callback
'HMGET', -- command
ip_check_table -- arguments
)
if not redis_ret then
rspamd_logger.infox(rspamd_config, "cannot check fwd_host redis map")
end
end,
priority = 18
})
rspamd_config:register_symbol({
name = 'TLS_HEADER',
type = 'postfilter',
@@ -390,9 +445,8 @@ rspamd_config:register_symbol({
local rspamd_logger = require "rspamd_logger"
local redis_params = rspamd_parse_redis_server('taghandler')
local rspamd_http = require "rspamd_http"
local rcpts = task:get_recipients('smtp')
local rcpts = task:get_recipients({'smtp', 'orig'})
local lua_util = require "lua_util"
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false)
@@ -417,8 +471,9 @@ rspamd_config:register_symbol({
-- Check if recipient has a tag (contains '+')
local tag = nil
if tagged_rcpt ~= nil then
tag = tagged_rcpt
local base_user, tag_part = rcpt_user:match('^([^%+]+)%+(.+)$')
if base_user and tag_part then
tag = tag_part
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
end
@@ -498,8 +553,7 @@ rspamd_config:register_symbol({
else
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
local sbj = task:get_header('Subject') or ''
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
@@ -907,6 +961,9 @@ rspamd_config:register_symbol({
if not seen_cte and rewrite.need_rewrite_ct then
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', new_cte)
task:set_milter_reply({
add_headers = {['Content-Transfer-Encoding'] = new_cte}
})
end
-- End of headers
+1 -1
View File
@@ -109,7 +109,7 @@ foreach ($rcpts as $rcpt) {
}
// Skip if domain is excluded
if (in_array($parsed_rcpt['domain'], $exclude_domains)) {
if (is_array($exclude_domains) && in_array($parsed_rcpt['domain'], $exclude_domains)) {
error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL);
continue;
}
+1 -1
View File
@@ -8,7 +8,7 @@ if(file_exists('inc/vars.local.inc.php')) {
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
header('Content-type: application/json');
if (strtolower($_GET['Protocol']) == 'activesync' && getenv('SKIP_SOGO') != "y") {
if (strtolower($_GET['Protocol']) == 'activesync' && $autodiscover_config['autodiscoverType'] == 'activesync' && getenv('SKIP_SOGO') != "y") {
echo '{"Protocol":"ActiveSync","Url":"' . $autodiscover_config['activesync']['url'] . '"}';
}
elseif (strtolower($_GET['Protocol']) == 'autodiscoverv1') {
+1 -1
View File
@@ -31,7 +31,7 @@ error_reporting(0);
$data = trim(file_get_contents("php://input"));
if (strpos($data, 'autodiscover/outlook/responseschema') !== false) {
if (strpos($data, 'autodiscover/outlook/responseschema') !== false && $autodiscover_config['autodiscoverType'] == 'activesync') {
$autodiscover_config['autodiscoverType'] = 'imap';
if ($autodiscover_config['useEASforOutlook'] == 'yes' &&
// Office for macOS does not support EAS
+7
View File
@@ -601,6 +601,13 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
// validate mailcow_password
$mailcow_password = $user_res['attributes']['mailcow_password'][0];
// Keycloak returns binary LDAP attributes like userPassword Base64 encoded
if (substr($mailcow_password, 0, 1) != '{') {
$decoded_password = base64_decode($mailcow_password, true);
if ($decoded_password !== false && substr($decoded_password, 0, 1) == '{') {
$mailcow_password = $decoded_password;
}
}
if (!verify_hash($mailcow_password, $pass)) {
return false;
}
+7
View File
@@ -35,6 +35,13 @@ function fwdhost($_action, $_data = null) {
);
return false;
}
$hosts = array_unique(array_map(function ($host) {
$net = explode('/', $host, 2);
if (filter_var($net[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$net[0] = inet_ntop(inet_pton($net[0]));
}
return implode('/', $net);
}, $hosts));
foreach ($hosts as $host) {
try {
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source);
+17 -5
View File
@@ -1416,11 +1416,11 @@ function set_tfa($_data) {
// Block TFA removal if force_tfa policy is active
$is_forced_tfa = false;
if ($_SESSION['mailcow_cc_role'] === 'user') {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
} else {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
}
@@ -1687,11 +1687,11 @@ function unset_tfa_key($_data) {
// Block key removal if force_tfa policy is active
$is_forced_tfa = false;
if ($_SESSION['mailcow_cc_role'] === 'user') {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
} else {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
}
@@ -3106,7 +3106,17 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
case "get-keycloak-admin-token":
// get access_token for service account of mailcow client
if ($iam_settings['authsource'] !== 'keycloak') return false;
if (isset($iam_settings['access_token'])) {
// only introspect a cached access_token that is not (about to be) expired,
// introspecting an expired token makes Keycloak log INTROSPECT_TOKEN_ERROR
$token_not_expired = false;
if (!empty($iam_settings['access_token'])) {
$token_parts = explode('.', $iam_settings['access_token']);
if (count($token_parts) === 3) {
$token_payload = json_decode(base64_decode(strtr($token_parts[1], '-_', '+/')), true);
$token_not_expired = is_array($token_payload) && isset($token_payload['exp']) && intval($token_payload['exp']) > time() + 30;
}
}
if ($token_not_expired) {
// check if access_token is valid
$url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token/introspect";
$req = http_build_query(array(
@@ -3157,6 +3167,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
':key' => 'access_token',
':value' => $res['access_token']
));
// keep the cached token in sync, so subsequent calls in the same run reuse it
$iam_settings['access_token'] = $res['access_token'];
return $res['access_token'];
break;
}
+44 -1
View File
@@ -2524,6 +2524,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain,
':tag_name' => $tag,
));
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
}
$_SESSION['return'][] = array(
@@ -2692,6 +2696,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain,
':tag_name' => $tag,
));
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
}
$_SESSION['return'][] = array(
@@ -3170,6 +3178,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':username' => $username,
':tag_name' => $tag,
));
$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = NOW() WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
} catch (Exception $e) {
}
}
@@ -3988,6 +4000,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
if (!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
continue;
}
// Keep old array for backward compatibility
$data['fixed_sender_aliases'][] = $row['address'];
// Split into allowed/blocked for proper display
@@ -4923,6 +4938,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
else if ($SaslLogs['service'] == 'SSO') {
$last_sso_login = strtotime($SaslLogs['datetime']);
}
else if ($SaslLogs['service'] == 'SOGO') {
$last_sogo_login = strtotime($SaslLogs['datetime']);
}
}
if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_imap_login = 0;
@@ -4936,10 +4954,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_sso_login = 0;
}
if (!isset($last_sogo_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_sogo_login = 0;
}
$mailboxdata['last_imap_login'] = $last_imap_login;
$mailboxdata['last_smtp_login'] = $last_smtp_login;
$mailboxdata['last_pop3_login'] = $last_pop3_login;
$mailboxdata['last_sso_login'] = $last_sso_login;
$mailboxdata['last_sogo_login'] = $last_sogo_login;
if (!isset($_extra) || $_extra != 'reduced') {
$rl = ratelimit('get', 'mailbox', $_data);
@@ -4977,7 +4999,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(':username' => $_data));
$sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($sender_acl_rows as $sender_acl_row) {
$mailboxdata['sender_acl'][] = $sender_acl_row['send_as'];
$send_as = $sender_acl_row['send_as'];
if ($_SESSION['mailcow_cc_role'] == 'domainadmin' && $send_as != '*') {
$in_scope = (substr($send_as, 0, 1) == '@')
? hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], substr($send_as, 1))
: hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $send_as);
if (!$in_scope) {
continue;
}
}
$mailboxdata['sender_acl'][] = $send_as;
}
}
$stmt = $pdo->prepare("SELECT `tag_name`
@@ -5916,6 +5947,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain,
':tag_name' => $tag,
));
if ($stmt->rowCount() > 0) {
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
}
}
}
@@ -5968,6 +6005,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':username' => $username,
':tag_name' => $tag,
));
if ($stmt->rowCount() > 0) {
$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = NOW() WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
}
}
}
+13 -6
View File
@@ -715,10 +715,6 @@ jQuery(function($){
$.each(json, function (i, item) {
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="domain_template" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
item.attributes.def_quota_for_mbox = humanFileSize(item.attributes.def_quota_for_mbox);
item.attributes.max_quota_for_mbox = humanFileSize(item.attributes.max_quota_for_mbox);
item.attributes.max_quota_for_domain = humanFileSize(item.attributes.max_quota_for_domain);
item.template = escapeHtml(item.template);
if (item.attributes.rl_frame === "s"){
item.attributes.rl_frame = lang_rl.second;
@@ -801,16 +797,25 @@ jQuery(function($){
title: lang.mailbox_defquota,
data: 'attributes.def_quota_for_mbox',
defaultContent: '',
render: function (data, type) {
return type === 'sort' || type === 'type' ? data : humanFileSize(data);
}
},
{
title: lang.max_quota,
data: 'attributes.max_quota_for_mbox',
defaultContent: '',
render: function (data, type) {
return type === 'sort' || type === 'type' ? data : humanFileSize(data);
}
},
{
title: lang.domain_quota_total,
data: 'attributes.max_quota_for_domain',
defaultContent: '',
render: function (data, type) {
return type === 'sort' || type === 'type' ? data : humanFileSize(data);
}
},
{
title: lang.gal,
@@ -932,6 +937,7 @@ jQuery(function($){
item.last_mail_login = (item.attributes.imap_access == 1 ? '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">IMAP @ ' + unix_time_format(Number(item.last_imap_login)) + '</div><br>' : '') +
(item.attributes.pop3_access == 1 ? '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">POP3 @ ' + unix_time_format(Number(item.last_pop3_login)) + '</div><br>' : '') +
(item.attributes.smtp_access == 1 ? '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">SMTP @ ' + unix_time_format(Number(item.last_smtp_login)) + '</div><br>' : '') +
'<div class="text-start badge bg-info mb-2" style="min-width: 70px;">SOGO @ ' + unix_time_format(Number(item.last_sogo_login)) + '</div><br>' +
'<div class="text-start badge bg-info" style="min-width: 70px;">SSO @ ' + unix_time_format(Number(item.last_sso_login)) + '</div>';
/*
if (!item.rl) {
@@ -1244,8 +1250,6 @@ jQuery(function($){
item.attributes.rl_value = (!item.attributes.rl_value) ? "∞" : escapeHtml(item.attributes.rl_value);
item.attributes.ratelimit = item.attributes.rl_value + " " + item.attributes.rl_frame;
item.attributes.quota = humanFileSize(item.attributes.quota);
item.attributes.tls_enforce_in = '<i class="text-' + (item.attributes.tls_enforce_in == 1 ? 'success bi bi-lock-fill' : 'danger bi bi-unlock-fill') + '"><span class="sorting-value">' + (item.attributes.tls_enforce_in == 1 ? '1' : '0') + '</span></i>';
item.attributes.tls_enforce_out = '<i class="text-' + (item.attributes.tls_enforce_out == 1 ? 'success bi bi-lock-fill' : 'danger bi bi-unlock-fill') + '"><span class="sorting-value">' + (item.attributes.tls_enforce_out == 1 ? '1' : '0') + '</span></i>';
item.attributes.pop3_access = '<i class="text-' + (item.attributes.pop3_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.pop3_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.pop3_access == 1 ? '1' : '0') + '</span></i>';
@@ -1332,6 +1336,9 @@ jQuery(function($){
title: lang.domain_quota,
data: 'attributes.quota',
defaultContent: '',
render: function (data, type) {
return type === 'sort' || type === 'type' ? data : humanFileSize(data);
}
},
{
title: lang.tls_enforce_in,
+6 -2
View File
@@ -1087,10 +1087,14 @@ if (isset($_GET['query'])) {
$data['mailboxes'][$mailbox][] = $mailbox;
$user_alias_details = user_get_alias_details($mailbox);
foreach ($user_alias_details['direct_aliases'] as $k => $v) {
$data['mailboxes'][$mailbox][] = $k;
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], substr(strrchr($k, '@'), 1))) {
$data['mailboxes'][$mailbox][] = $k;
}
}
foreach ($user_alias_details['shared_aliases'] as $k => $v) {
$data['mailboxes'][$mailbox][] = $k;
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], substr(strrchr($k, '@'), 1))) {
$data['mailboxes'][$mailbox][] = $k;
}
}
}
}
-5
View File
@@ -1074,7 +1074,6 @@
"verified_webauthn_login": "Inicio de sesión WebAuthn verificado",
"verified_yotp_login": "Inicio de sesión Yubico OTP verificado",
"admin_added": "Se agregó el administrador %s",
"admin_api_modified": "Se guardaron los cambios en la API",
"admin_removed": "Se eliminó el administrador %s",
"app_links": "Se guardaron los cambios en los enlaces de aplicaciones",
"app_passwd_added": "Se agregó una nueva contraseña de aplicación",
@@ -1333,10 +1332,6 @@
"sogo_profile_reset_help": "Esto destruirá el perfil de SOGo del usuario y <b>eliminará de forma irrecuperable todos sus contactos y datos de calendario</b>.",
"spam_score_reset": "Restablecer al valor predeterminado del servidor",
"spamfilter_table_domain_policy": "n/d (política del dominio)",
"syncjob_check_log": "Revisar registro",
"syncjob_EX_OK": "Correcto",
"syncjob_EXIT_OVERQUOTA": "El buzón de destino superó su cuota",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Usuario o contraseña incorrectos",
"tfa_info": "La autenticación de dos factores ayuda a proteger su cuenta. Si la activa, necesitará contraseñas de aplicación para iniciar sesión en aplicaciones o servicios que no admiten la autenticación de dos factores (p. ej., clientes de correo)."
},
"warning": {
-1
View File
@@ -95,7 +95,6 @@
"bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.<br>Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.",
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
"tags": "Tag",
"dry": "Simula sincronizzazione",
"internal": "Interno",
"internal_info": "Gli alias interni sono accessibili solo dal proprio dominio o dai domini alias.",
"sender_allowed": "Consenti l'invio con questo alias"
+5 -3
View File
@@ -97,9 +97,11 @@
<div class="col-sm-10">
<select data-live-search="true" data-width="100%" style="width:100%" id="editSelectSenderACL" name="sender_acl" size="10" multiple>
{% for domain in sender_acl_handles.sender_acl_domains.ro %}
<option data-subtext="Admin" value="{{ domain }}" disabled selected>
{{ lang.edit.dont_check_sender_acl|format(domain) }}
</option>
{% if domain == '*' or mailcow_cc_role == 'admin' %}
<option data-subtext="Admin" value="{{ domain }}" disabled selected>
{{ lang.edit.dont_check_sender_acl|format(domain) }}
</option>
{% endif %}
{% endfor %}
{% for domain in sender_acl_handles.sender_acl_domains.rw %}
<option value="{{ domain }}" selected>
+5 -5
View File
@@ -68,7 +68,7 @@ services:
- redis
clamd-mailcow:
image: ghcr.io/mailcow/clamd:1.4.6-1
image: ghcr.io/mailcow/clamd:1.4.6-2
restart: always
depends_on:
unbound-mailcow:
@@ -348,7 +348,7 @@ services:
- dovecot
postfix-mailcow:
image: ghcr.io/mailcow/postfix:3.10.12-1
image: ghcr.io/mailcow/postfix:3.10.13-1
depends_on:
mysql-mailcow:
condition: service_started
@@ -391,7 +391,7 @@ services:
- postfix
postfix-tlspol-mailcow:
image: ghcr.io/mailcow/postfix-tlspol:1.14.0
image: ghcr.io/mailcow/postfix-tlspol:1.14.5
depends_on:
unbound-mailcow:
condition: service_healthy
@@ -428,7 +428,7 @@ services:
- php-fpm-mailcow
- sogo-mailcow
- rspamd-mailcow
image: ghcr.io/mailcow/nginx:1.30.6-2
image: ghcr.io/mailcow/nginx:1.30.6-3
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
@@ -612,7 +612,7 @@ services:
- watchdog
dockerapi-mailcow:
image: ghcr.io/mailcow/dockerapi:2.15
image: ghcr.io/mailcow/dockerapi:2.16
security_opt:
- label=disable
restart: always