Compare commits

..
15 Commits
Author SHA1 Message Date
FreddleSpl0itandGitHub f4961c4023 Merge pull request #7333 from fallmo/fix/cors-settings-validation
fix: cors allowed origins settings validation
2026-07-28 15:35:29 +02:00
FreddleSpl0itandGitHub 38de21592c Merge pull request #7358 from SYNLINQ/staging
Fix nginx CVE-2026-42533
2026-07-28 14:40:30 +02:00
FreddleSpl0it f44bd2f36a [Web] document sender_acl in get/mailbox API examples 2026-07-28 14:19:45 +02:00
FreddleSpl0itandGitHub 95a77f2dcb Merge pull request #7348 from smpaz7467/fix/api-get-mailbox-sender-acl
[Web] return sender_acl in get/mailbox API
2026-07-28 14:12:57 +02:00
FreddleSpl0itandGitHub e856510fb2 Merge pull request #7345 from smpaz7467/fix/time-limited-alias-api
[Web] fix add/time_limited_alias silently discarding requests and validity
2026-07-28 13:49:37 +02:00
FreddleSpl0itandGitHub d51d06d716 Merge pull request #7343 from smpaz7467/fix/nginx-ipv6-default-server
[Nginx] only bind IPv6 default_server when ENABLE_IPV6 is set
2026-07-28 13:35:17 +02:00
renovate[bot]andGitHub b4bb1a625b Update actions/stale action to v11 (#7375) 2026-07-28 11:07:20 +02:00
FreddleSpl0itandGitHub 2e5a29bf69 Merge pull request #7367 from oidipos/fix/quarantine-subject
fix: restore subject display in quarantine overview
2026-07-28 10:54:23 +02:00
oidipos c877fdf0a5 fix: remove MIME decoding of JSON encoded subject
Since moving to rspamd's multipart metadata_exporter,
`subject` is a JSON encoded UTF-8 string and must not be MIME decoded.
2026-07-24 21:03:00 +02:00
SYNLINQandGitHub 8c9524a2fe Update docker-compose.yml
reference patched nginx image
2026-07-20 21:32:57 +02:00
SYNLINQandGitHub 2ebd32d2ee Update Dockerfile
bump nginx version to 1.30.4
2026-07-20 21:31:57 +02:00
Stephen RitzandClaude Opus 4.8 14772c3a20 [Web] return sender_acl in get/mailbox API
sender_acl can be set through edit/mailbox but was never returned by
get/mailbox, so an API client could not read back what it had written,
and get/mailbox/all / get/mailbox/{mailbox} both omitted it.

Add the mailbox's internal send-as ACL (the sender_acl table rows with
external = 0) to mailbox_details as sender_acl, an array of send_as
values, mirroring the field edit/mailbox accepts. It is added in the same
block as the other detailed fields, so the lightweight get/mailbox/reduced
endpoint is unaffected.

Fixes #7011

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:36:40 -07:00
Stephen RitzandClaude Opus 4.8 c17cc8a792 [Web] fix add/time_limited_alias silently discarding requests and validity
Three defects in add/time_limited_alias:

The description was read as $_data['description'] without a guard. When a
client omits it, null is bound to spamalias.description, which is TEXT NOT
NULL, so the insert raises a PDOException. The global exception handler is
terminal, so process_add_return() never echoes anything and the caller sees
HTTP 200 with an empty body while no alias was created. Default it to an
empty string instead.

The validity guard used a single condition whose else branch also caught the
success case, so every valid validity was overwritten with the 8760 hour
default and the parameter did nothing. Only invalid values were rejected.
Nest the range check so a valid value survives.

The OpenAPI spec documented only username and domain, while the code also
reads description, validity and permanent. Spec driven clients therefore
could not construct a working request. Document all three.

spamalias.description is the only NOT NULL description column in the schema,
which is why the same unguarded read in add/domain and add/resource does not
fail, both of those columns are nullable.

This does not change the generic exception handling. A database error is
still swallowed into an empty HTTP 200, and the message the handler builds
carries the raw PDOException, so surfacing it to API clients would need
sanitising first. That is left for a separate change.

Refs #7287

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:23:31 -07:00
Stephen RitzandClaude Opus 4.8 7dab6c63d1 [Nginx] only bind IPv6 default_server when ENABLE_IPV6 is set
The HTTP-to-HTTPS redirect server block bound `listen [::]:{{ HTTP_PORT }}
default_server` unconditionally, while every other IPv6 listen directive in
this template is guarded by `{% if ENABLE_IPV6 %}`. On hosts where IPv6 is
disabled at the kernel level, nginx cannot bind `::` and fails to start.

This only triggers when HTTP_REDIRECT is enabled and ENABLE_IPV6 is false,
which is why it survives testing with ENABLE_IPV6=false alone.

Guard the directive like the other six IPv6 listeners in the template.

Refs #7296

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:54:28 -07:00
Mohamed Fall 6f7fee49cd fix: cors allowed origins settings validation 2026-07-13 15:29:48 +00:00
15 changed files with 89 additions and 82 deletions
@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v10.4.0
uses: actions/stale@v11.0.0
with:
repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60
+1 -1
View File
@@ -1,4 +1,4 @@
FROM nginx:1.30.3-alpine
FROM nginx:1.30.4-alpine
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ENV PIP_BREAK_SYSTEM_PACKAGES=1
+2 -5
View File
@@ -42,17 +42,14 @@ http {
https https;
}
map $server_port $sogo_auth_internal {
65510 "1";
default "";
}
{% if HTTP_REDIRECT %}
# HTTP to HTTPS redirect
server {
root /web;
listen {{ HTTP_PORT }} default_server;
{% if ENABLE_IPV6 %}
listen [::]:{{ HTTP_PORT }} default_server;
{%endif%}
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
@@ -116,8 +116,6 @@ location ~ \.php$ {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600;
}
+1 -1
View File
@@ -53,7 +53,7 @@ $raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
$raw_size = (int)$_FILES['message']['size'];
$qid = $meta['qid'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$subject = $meta['subject'] ?? '';
$score = $meta['score'] ?? 0;
$rcpts = $meta['rcpt'] ?? array();
$user = $meta['user'] ?? 'unknown';
+1 -1
View File
@@ -50,7 +50,7 @@ $qid = $meta['qid'] ?? 'unknown';
$rcpts = $meta['rcpt'] ?? array();
$sender = $meta['from'] ?? '';
$ip = $meta['ip'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$subject = $meta['subject'] ?? '';
$messageid= $meta['message_id'] ?? '';
$priority = 0;
+16 -2
View File
@@ -185,6 +185,9 @@ paths:
example:
username: info@domain.tld
domain: domain.tld
description: my time limited alias
validity: 8760
permanent: false
properties:
username:
description: 'the mailbox an alias should be created for'
@@ -192,6 +195,15 @@ paths:
domain:
description: "the domain"
type: string
description:
description: "a description for the alias, defaults to an empty string"
type: string
validity:
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
type: integer
permanent:
description: "keep the alias after it expired, defaults to false"
type: boolean
type: object
summary: Create time limited alias
/api/v1/add/app-passwd:
@@ -4908,6 +4920,7 @@ paths:
quota: 3221225472
quota_used: 0
rl: false
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
spam_aliases: 0
username: info@doman3.tld
tags: ["tag1", "tag2"]
@@ -5834,6 +5847,7 @@ paths:
quota: 3221225472
quota_used: 0
rl: false
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
spam_aliases: 0
username: info@domain3.tld
tags: ["tag1", "tag2"]
@@ -5856,7 +5870,7 @@ paths:
response:
value:
- type: "success"
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
msg: "cors_headers_edited"
description: OK
headers: { }
@@ -5873,7 +5887,7 @@ paths:
schema:
example:
attr:
allowed_origins: ["*", "mail.mailcow.tld"]
allowed_origins: ["*", "https://mail.mailcow.tld"]
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
properties:
attr:
+3 -14
View File
@@ -429,25 +429,14 @@ function domain_admin_sso($_action, $_data) {
switch ($_action) {
case 'check':
$token = preg_replace('/[^a-zA-Z0-9-]/', '', $_data);
$token = $_data;
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
$stmt->execute(array(
':token' => $token
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
));
$return = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($return['username'])) {
return false;
}
// single-use: consume the token; if a concurrent request already used it, deny
$del = $pdo->prepare("DELETE FROM `da_sso` WHERE `token` = :token");
$del->execute(array(
':token' => $token
));
if ($del->rowCount() < 1) {
return false;
}
return $return['username'];
return empty($return['username']) ? false : $return['username'];
case 'issue':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
+31 -3
View File
@@ -53,6 +53,28 @@ function valid_network($network) {
function valid_hostname($hostname) {
return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
}
// Validates a browser-style Origin: scheme://host[:port], no path/query/fragment/credentials
function valid_origin($origin) {
$parts = parse_url($origin);
if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) {
return false;
}
if (!in_array(strtolower($parts['scheme']), array('http', 'https'), true)) {
return false;
}
if (isset($parts['user']) || isset($parts['pass']) || isset($parts['path']) || isset($parts['query']) || isset($parts['fragment'])) {
return false;
}
$host = $parts['host'];
$host_without_brackets = trim($host, '[]');
if (!valid_hostname($host_without_brackets) && !filter_var($host_without_brackets, FILTER_VALIDATE_IP)) {
return false;
}
// Reject anything that doesn't round-trip to the exact same origin (e.g. stray trailing slash)
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
$rebuilt = strtolower($parts['scheme']) . '://' . strtolower($host) . $port;
return strtolower($origin) === $rebuilt;
}
// Thanks to https://stackoverflow.com/a/49373789
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
function ip_acl($ip, $networks) {
@@ -2283,10 +2305,13 @@ function cors($action, $data = null) {
return false;
}
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array(getBaseURL());
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
foreach ($allowed_origins as $origin) {
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
foreach ($allowed_origins as &$origin) {
if ($origin === '*') {
continue;
}
if (!valid_origin($origin)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $data),
@@ -2294,7 +2319,10 @@ function cors($action, $data = null) {
);
return false;
}
// browsers always send a lowercase scheme/host in the Origin header, so normalize to match
$origin = strtolower($origin);
}
unset($origin);
$allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
+22 -20
View File
@@ -41,13 +41,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
else {
$username = $_SESSION['mailcow_cc_username'];
}
if (isset($_data["validity"]) && !filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'validity_missing'
);
return false;
if (isset($_data["validity"])) {
if (!filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'validity_missing'
);
return false;
}
}
else {
// Default to 1 yr
@@ -60,7 +62,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$permanent = 0;
}
$domain = $_data['domain'];
$description = $_data['description'];
// spamalias.description is NOT NULL, an absent description must not become a null insert
$description = $_data['description'] ?? '';
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
$valid_alias_domains = user_get_alias_details($username)['alias_domains'];
if (!empty($valid_alias_domains)) {
@@ -5320,6 +5323,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['rl_scope'] = 'domain';
}
$mailboxdata['is_relayed'] = $row['backupmx'];
// Internal send-as ACL for this mailbox, mirrors the sender_acl field accepted by edit/mailbox
$mailboxdata['sender_acl'] = array();
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '0'");
$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'];
}
}
$stmt = $pdo->prepare("SELECT `tag_name`
FROM `tags_mailbox` WHERE `username`= :username");
@@ -5647,11 +5658,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
$stmt->execute(array(
':c_object_like' => $c_object_like,
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
@@ -6047,11 +6055,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username");
$stmt->execute(array(
':c_object_like' => $c_object_like,
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
@@ -6207,11 +6212,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $name
));
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
$c_object_like = '%/' . addcslashes($name, '\\%_') . '/%';
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username");
$stmt->execute(array(
':c_object_like' => $c_object_like,
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+8 -12
View File
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
return false;
}
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE `qhash` = :hash
WHERE `qhash` = :hash
AND user_acl.quarantine = 1
AND rcpt IN (SELECT username FROM mailbox)');
$stmt->execute(array(':hash' => $hash));
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
return false;
}
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE `qhash` = :hash
WHERE `qhash` = :hash
AND `user_acl`.`quarantine` = 1
AND `username` IN (SELECT `username` FROM `mailbox`)');
$stmt->execute(array(':hash' => $hash));
@@ -170,8 +170,6 @@ function quarantine($_action, $_data = null) {
}
elseif ($release_format == 'raw') {
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
// dot-stuffing per RFC5321 4.5.2: escape leading dots
$detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
@@ -182,7 +180,7 @@ function quarantine($_action, $_data = null) {
array('221', '')
);
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
if (!$smtp_connection) {
logger(array('return' => array(
array(
@@ -194,7 +192,7 @@ function quarantine($_action, $_data = null) {
return false;
}
for ($i=0; $i < count($postfix_talk); $i++) {
$smtp_resource = fgets($smtp_connection, 256);
$smtp_resource = fgets($smtp_connection, 256);
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
$ret = substr($smtp_resource, 0, 3);
$ret = (empty($ret)) ? '-' : $ret;
@@ -467,19 +465,17 @@ function quarantine($_action, $_data = null) {
}
elseif ($release_format == 'raw') {
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
// dot-stuffing per RFC5321 4.5.2: escape leading dots
$row['msg'] = preg_replace('~^\.~m', '..', $row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
array('250', 'DATA' . chr(10)),
array('354', $row['msg'] . chr(10) . '.' . chr(10)),
array('354', str_replace("\n.", '', $row['msg']) . chr(10) . '.' . chr(10)),
array('250', 'QUIT' . chr(10)),
array('221', '')
);
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
if (!$smtp_connection) {
$_SESSION['return'][] = array(
'type' => 'warning',
@@ -489,7 +485,7 @@ function quarantine($_action, $_data = null) {
return false;
}
for ($i=0; $i < count($postfix_talk); $i++) {
$smtp_resource = fgets($smtp_connection, 256);
$smtp_resource = fgets($smtp_connection, 256);
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
$ret = substr($smtp_resource, 0, 3);
$ret = (empty($ret)) ? '-' : $ret;
@@ -837,7 +833,7 @@ function quarantine($_action, $_data = null) {
)));
return false;
}
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
$stmt->execute(array(':hash' => $hash));
return $stmt->fetch(PDO::FETCH_ASSOC);
break;
-7
View File
@@ -1089,8 +1089,6 @@ jQuery(function($){
return str;
}).join('<br>\n');
item.subject = escapeHtml(item.subject);
if (item.sender_mime != null) item.sender_mime = escapeHtml(item.sender_mime);
if (item['message-id'] != null) item['message-id'] = escapeHtml(item['message-id']);
var scan_time = item.time_real.toFixed(3);
if (item.time_virtual) {
scan_time += ' / ' + item.time_virtual.toFixed(3);
@@ -1214,11 +1212,6 @@ jQuery(function($){
});
} else if (table == 'rllog') {
$.each(data, function (i, item) {
if (item.header_from != null) item.header_from = escapeHtml(item.header_from);
if (item.header_subject != null) item.header_subject = escapeHtml(item.header_subject);
if (item.from != null) item.from = escapeHtml(item.from);
if (item.rcpt != null) item.rcpt = escapeHtml(item.rcpt);
if (item.message_id != null) item.message_id = escapeHtml(item.message_id);
if (item.user == null) {
item.user = "none";
}
-1
View File
@@ -996,7 +996,6 @@ jQuery(function($){
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'
};
item.username = escapeHtml(item.username);
item.name = escapeHtml(item.name);
if (Array.isArray(item.tags)){
var tags = '';
+2 -11
View File
@@ -8,12 +8,8 @@ $ALLOW_ADMIN_EMAIL_LOGIN = (preg_match(
$session_var_user_allowed = 'sogo-sso-user-allowed';
$session_var_pass = 'sogo-sso-pass';
// only the internal nginx auth_request loopback (127.0.0.1:65510) sets this;
// external clients can never supply it (bare fastcgi param, not an HTTP header)
$is_internal_auth = (($_SERVER['SOGO_AUTH_INTERNAL'] ?? '') === '1');
// validate credentials for basic auth requests
if ($is_internal_auth && isset($_SERVER['PHP_AUTH_USER'])) {
if (isset($_SERVER['PHP_AUTH_USER'])) {
// load prerequisites only when required
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
@@ -54,11 +50,6 @@ elseif (isset($_GET['login'])) {
(($_SESSION['acl']['login_as'] == "1" && $ALLOW_ADMIN_EMAIL_LOGIN !== 0) || ($is_dual === false && $login == $_SESSION['mailcow_cc_username']))) {
if (filter_var($login, FILTER_VALIDATE_EMAIL)) {
if (user_get_alias_details($login) !== false) {
// enforce tenant boundary
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $login)) {
header("Location: /");
exit;
}
// Block SOGo access if pending actions (2FA setup, password update)
if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) {
header("Location: /");
@@ -89,7 +80,7 @@ elseif (isset($_GET['login'])) {
exit;
}
// only check for admin-login on sogo GUI requests
elseif ($is_internal_auth && isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) {
elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) {
// this is an nginx auth_request call, we check for existing sogo-sso session variables
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php')) {
+1 -1
View File
@@ -419,7 +419,7 @@ services:
- php-fpm-mailcow
- sogo-mailcow
- rspamd-mailcow
image: ghcr.io/mailcow/nginx:1.30.3-1
image: ghcr.io/mailcow/nginx:1.30.4
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment: