1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-06-15 11:00:29 +00:00

Compare commits

..

24 Commits

Author SHA1 Message Date
renovate[bot] 7e6c36dce1 Update alpine Docker tag to v3.24 (#7280) 2026-06-11 11:35:03 +02:00
FreddleSpl0it fa154c71f3 [PHP] Rebuild Image 2026-06-11 10:29:01 +02:00
FreddleSpl0it 83a045ed3e Merge pull request #7193 from mailcow/renovate/composer-composer-2.x
Update dependency composer/composer to v2.10.1
2026-06-11 10:07:19 +02:00
FreddleSpl0it 064817ac70 Merge pull request #7189 from mailcow/renovate/php-pecl-mail-mailparse-3.x
Update dependency php/pecl-mail-mailparse to v3.2.0
2026-06-11 10:06:49 +02:00
FreddleSpl0it 2bd7a24b8c Merge pull request #7212 from mailcow/mkuron-patch-mobileconfig
Escape generated password in mobileconfig
2026-06-11 10:05:17 +02:00
FreddleSpl0it ecc2462e4c Merge pull request #7267 from goodygh/7249-update-sogo
[SOGo] Update to 5.12.9
2026-06-11 10:04:30 +02:00
FreddleSpl0it 2d8db72d46 Merge pull request #7275 from Snafu/fix/admin-mailbox-tfa-missing
Fix force_tfa not available in mailbox template #7216
2026-06-11 10:03:39 +02:00
FreddleSpl0it 277a307fb9 [Web] Fix refresh SOGo view on mailbox deletion 2026-06-11 09:55:32 +02:00
FreddleSpl0it d455555621 Merge pull request #7277 from ibobgunardi/bobi/mailcow-7136-sogo-active-refresh
Refresh SOGo view after mailbox activation
2026-06-11 09:53:30 +02:00
FreddleSpl0it 9ea2ff1dff Merge pull request #7283 from mailcow/feat/update-metadata-exporter
[Rspamd] Migrate metadata_exporter to multipart formatter
2026-06-11 09:40:34 +02:00
FreddleSpl0it 24cc369d84 [Rspamd] Migrate metadata_exporter to multipart formatter 2026-06-11 09:34:27 +02:00
FreddleSpl0it 5f5367f2f9 Merge pull request #7172 from mailcow/dragoangel-patch-4
Update RSPAMD version to 4.1.0 in Dockerfile
2026-06-11 09:04:19 +02:00
milkmaker 03c31f825a update postscreen_access.cidr (#7269) 2026-06-09 12:20:53 +02:00
renovate[bot] c0050e8836 Update devops-infra/action-pull-request action to v1.3.0 (#7272) 2026-06-09 12:19:09 +02:00
Dmytro Alieksieiev 15891960f7 Update RSPAMD version to 4.1.0 2026-06-09 01:19:06 +02:00
Bobby cce02e2b15 Refresh SOGo view after mailbox activation 2026-06-08 00:01:43 +07:00
Snafu 0fafda696b Fix force_tfa not available in mailbox template #7216 2026-06-07 17:03:52 +02:00
renovate[bot] 843db5854c Update dependency composer/composer to v2.10.1
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-06-04 11:04:42 +00:00
goodygh 23e4e4f373 [SOGo] Update to 5.12.9 2026-05-29 01:39:38 +02:00
Michael Kuron ffbc37a00c Escape generated password in mobileconfig
Escape ampersand, less than, greater than to avoid generating invalid XML.

Fixes #7171
2026-05-24 11:52:12 +02:00
renovate[bot] 104af58628 Update dependency php/pecl-mail-mailparse to v3.2.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-05-14 18:33:03 +00:00
Dmitriy Alekseev b0873edb6a Update RSPAMD version in Dockerfile 2026-04-06 17:28:32 +02:00
Dmitriy Alekseev 694d751915 Update RSPAMD version in Dockerfile 2026-04-06 17:28:14 +02:00
Dmitriy Alekseev 08278a8be9 Update RSPAMD version to 4.0.0 in Dockerfile 2026-03-30 23:51:05 +02:00
15 changed files with 98 additions and 76 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
with:
fetch-depth: 0
- name: Run the Action
uses: devops-infra/action-pull-request@v1.2.1
uses: devops-infra/action-pull-request@v1.3.0
with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
+2 -2
View File
@@ -7,13 +7,13 @@ ARG APCU_PECL_VERSION=5.1.28
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.1
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.9
ARG MAILPARSE_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.4.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.3.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.9.5
ARG COMPOSER_VERSION=2.10.1
RUN apk add -U --no-cache autoconf \
aspell-dev \
+1 -1
View File
@@ -2,7 +2,7 @@ FROM debian:trixie-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.14.3-1~236eb65
ARG RSPAMD_VER=rspamd_4.1.0-1~e2b0b18
ARG CODENAME=trixie
ENV LC_ALL=C
+3 -3
View File
@@ -1,6 +1,6 @@
# SOGo built from source to enable security patch application
# Repository: https://github.com/Alinto/sogo
# Version: SOGo-5.12.8
# Version: SOGo-5.12.9
#
# Applied security patches:
# -
@@ -12,8 +12,8 @@ FROM debian:bookworm
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_VERSION=SOGo-5.12.8
ARG SOPE_VERSION=SOPE-5.12.8
ARG SOGO_VERSION=SOGo-5.12.9
ARG SOPE_VERSION=SOPE-5.12.9
# Security patches to apply (space-separated commit hashes)
ARG SOGO_SECURITY_PATCHES=""
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
@@ -3,8 +3,7 @@ rules {
backend = "http";
url = "http://nginx:9081/pipe.php";
selector = "reject_no_global_bl";
formatter = "default";
meta_headers = true;
formatter = "multipart";
}
RLINFO {
backend = "http";
@@ -16,8 +15,7 @@ rules {
backend = "http";
url = "http://nginx:9081/pushover.php";
selector = "mailcow_rcpt";
formatter = "json";
meta_headers = true;
formatter = "multipart";
}
}
+27 -32
View File
@@ -32,47 +32,42 @@ function parse_email($email) {
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
// rspamd metadata_exporter (multipart formatter):
// - $_POST['metadata'] JSON with the rspamd metadata
// - $_FILES['message'] raw RFC822 message
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
http_response_code(400);
exit;
}
$raw_data_content = file_get_contents('php://input');
$meta = json_decode($_POST['metadata'], true);
if (!is_array($meta)) {
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
http_response_code(400);
exit;
}
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
$headers = getallheaders();
$raw_size = (int)$_FILES['message']['size'];
$qid = $headers['X-Rspamd-Qid'];
$fuzzy = $headers['X-Rspamd-Fuzzy'];
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$score = $headers['X-Rspamd-Score'];
$rcpts = $headers['X-Rspamd-Rcpt'];
$user = $headers['X-Rspamd-User'];
$ip = $headers['X-Rspamd-Ip'];
$action = $headers['X-Rspamd-Action'];
$sender = $headers['X-Rspamd-From'];
$symbols = $headers['X-Rspamd-Symbols'];
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
$qid = $meta['qid'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$score = $meta['score'] ?? 0;
$rcpts = $meta['rcpt'] ?? array();
$user = $meta['user'] ?? 'unknown';
$ip = $meta['ip'] ?? 'unknown';
$action = $meta['action'] ?? 'no action';
$sender = $meta['from'] ?? '';
$symbols = json_encode($meta['symbols'] ?? array());
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
if (empty($sender)) {
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
$sender = 'empty-env-from@localhost';
}
if ($fuzzy == 'unknown') {
$fuzzy = '[]';
}
try {
$max_size = (int)$redis->Get('Q_MAX_SIZE');
if (($max_size * 1048576) < $raw_size) {
@@ -94,7 +89,7 @@ catch (RedisException $e) {
$rcpt_final_mailboxes = array();
// Loop through all rcpts
foreach (json_decode($rcpts, true) as $rcpt) {
foreach ($rcpts as $rcpt) {
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+22 -26
View File
@@ -32,50 +32,46 @@ function parse_email($email) {
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
if (empty($_POST['metadata'])) {
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
http_response_code(400);
exit;
}
$headers = getallheaders();
$json_body = json_decode(file_get_contents('php://input'));
$meta = json_decode($_POST['metadata'], true);
if (!is_array($meta)) {
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
http_response_code(400);
exit;
}
$qid = $headers['X-Rspamd-Qid'];
$rcpts = $headers['X-Rspamd-Rcpt'];
$sender = $headers['X-Rspamd-From'];
$ip = $headers['X-Rspamd-Ip'];
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$messageid= $json_body->message_id;
$qid = $meta['qid'] ?? 'unknown';
$rcpts = $meta['rcpt'] ?? array();
$sender = $meta['from'] ?? '';
$ip = $meta['ip'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$messageid= $meta['message_id'] ?? '';
$priority = 0;
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
$symbols_array = $meta['symbols'] ?? array();
if (is_array($symbols_array)) {
foreach ($symbols_array as $symbol) {
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
$priority = 1;
break;
}
}
}
$sender_address = $json_body->header_from[0];
$sender_address = $meta['header_from'][0] ?? '';
$sender_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
$sender_address = $matches['address'];
$sender_name = trim($matches['name'], '"\' ');
}
$to_address = $json_body->header_to[0];
$to_address = $meta['header_to'][0] ?? '';
$to_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
$to_address = $matches['address'];
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
$rcpt_final_mailboxes = array();
// Loop through all rcpts
foreach (json_decode($rcpts, true) as $rcpt) {
foreach ($rcpts as $rcpt) {
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+13 -1
View File
@@ -1072,6 +1072,7 @@ paths:
password2: "*"
quota: "3072"
force_pw_update: "1"
force_tfa: "1"
tls_enforce_in: "1"
tls_enforce_out: "1"
tags: ["tag1", "tag2"]
@@ -1118,6 +1119,7 @@ paths:
password2: atedismonsin
quota: "3072"
force_pw_update: "1"
force_tfa: "1"
tls_enforce_in: "1"
tls_enforce_out: "1"
tags: ["tag1", "tag2"]
@@ -1151,6 +1153,9 @@ paths:
force_pw_update:
description: forces the user to update its password on first login
type: boolean
force_tfa:
description: force 2FA enrollment at login
type: boolean
tls_enforce_in:
description: force inbound email tls encryption
type: boolean
@@ -2510,7 +2515,7 @@ paths:
description: >-
Using this endpoint you can perform actions on quarantine items. It is possible to release
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
You must provide the quarantine item IDs. You can get the IDs using the GET method.
You must provide the quarantine item IDs. You can get the IDs using the GET method.
operationId: Edit mails in Quarantine
requestBody:
content:
@@ -3414,6 +3419,7 @@ paths:
- mailbox
- active: "1"
force_pw_update: "0"
force_tfa: "0"
name: Full name
password: "*"
password2: "*"
@@ -3464,6 +3470,7 @@ paths:
attr:
active: "1"
force_pw_update: "0"
force_tfa: "0"
name: Full name
authsource: mailcow
password: ""
@@ -3487,6 +3494,9 @@ paths:
force_pw_update:
description: force user to change password on next login
type: boolean
force_tfa:
description: force 2FA enrollment at login
type: boolean
name:
description: Full name of the mailbox user
type: string
@@ -4881,6 +4891,7 @@ paths:
- active: "1"
attributes:
force_pw_update: "0"
force_tfa: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
@@ -5805,6 +5816,7 @@ paths:
- active: "1"
attributes:
force_pw_update: "0"
force_tfa: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
+1 -2
View File
@@ -3505,7 +3505,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $username;
}
return true;
break;
case 'mailbox_rename':
$domain = $_data['domain'];
@@ -3828,6 +3827,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : $is_now['force_tfa'];
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
@@ -6127,7 +6127,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $username;
}
return true;
break;
case 'mailbox_templates':
if ($_SESSION['mailcow_cc_role'] != "admin") {
+11
View File
@@ -424,6 +424,11 @@ $(document).ready(function() {
} else {
$('#force_pw_update').prop('checked', false);
}
if (template.force_tfa == 1){
$('#force_tfa').prop('checked', true);
} else {
$('#force_tfa').prop('checked', false);
}
if (template.sogo_access == 1){
$('#sogo_access').prop('checked', true);
} else {
@@ -1242,6 +1247,7 @@ jQuery(function($){
item.attributes.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.eas_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.dav_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.force_tfa = '<i class="text-' + (item.attributes.force_tfa == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.force_tfa == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.force_tfa == 1 ? '1' : '0') + '</span></i>';
if (item.attributes.quarantine_notification === 'never') {
item.attributes.quarantine_notification = lang.never;
} else if (item.attributes.quarantine_notification === 'hourly') {
@@ -1385,6 +1391,11 @@ jQuery(function($){
return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.force_tfa,
data: 'attributes.force_tfa',
defaultContent: ''
},
{
title: lang_edit.ratelimit,
data: 'attributes.ratelimit',
+1
View File
@@ -929,6 +929,7 @@
"filters": "Filters",
"fname": "Full name",
"force_pw_update": "Force password update at next login",
"force_tfa": "TFA",
"gal": "Global Address List",
"goto_ham": "Learn as <b>ham</b>",
"goto_spam": "Learn as <b>spam</b>",
+1
View File
@@ -65,6 +65,7 @@ if (isset($_GET['app_password'])) {
$attr['protocols'][] = 'dav_access';
}
app_passwd("add", $attr);
$password = htmlspecialchars($password, ENT_NOQUOTES);
} else {
$app_password = false;
}
@@ -8,6 +8,7 @@
<input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="force_tfa">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
@@ -165,6 +166,14 @@
</div>
</div>
</div>
<div class="row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="force_tfa" id="force_tfa"{% if template.attributes.force_tfa == '1' %} checked{% endif %}> {{ lang.tfa.force_tfa }}</label>
<small class="text-muted">{{ lang.tfa.force_tfa_info }}</small>
</div>
</div>
</div>
{% if not skip_sogo %}
<div class="row">
<div class="offset-sm-2 col-sm-10">
+3 -3
View File
@@ -84,7 +84,7 @@ services:
- clamd
rspamd-mailcow:
image: ghcr.io/mailcow/rspamd:3.14.3-1
image: ghcr.io/mailcow/rspamd:4.1.0-1
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
@@ -117,7 +117,7 @@ services:
- rspamd
php-fpm-mailcow:
image: ghcr.io/mailcow/phpfpm:8.2.29-2
image: ghcr.io/mailcow/phpfpm:8.2.29-3
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on:
- redis-mailcow
@@ -200,7 +200,7 @@ services:
- phpfpm
sogo-mailcow:
image: ghcr.io/mailcow/sogo:5.12.8-1
image: ghcr.io/mailcow/sogo:5.12.9-1
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@@ -25,6 +25,6 @@ services:
- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
mysql-mailcow:
image: alpine:3.23
image: alpine:3.24
command: /bin/true
restart: "no"