Merge branch 'staging' into nightly

This commit is contained in:
FreddleSpl0it committed 2026-11-10 23:09:22 +01:00
commit f47dc41e8e
33 files changed
+355 -145

No files matched your search

+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);
+31 -7
View File
@@ -1399,6 +1399,18 @@ function set_tfa($_data) {
case "webauthn":
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
// Refuse incomplete WebAuthn registrations. Without this guard a request
// that does not contain the parsed credential data (e.g. a plain form
// POST) would silently insert an empty keyHandle while reporting success.
if (empty($_data['registration']) || empty($_data['registration']->credentialId)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'webauthn_registration_failed'
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
$stmt->execute(array(
@@ -1421,11 +1433,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');
}
@@ -1692,11 +1704,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');
}
@@ -2425,7 +2437,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
case "use_ssl":
case "use_tls":
case "login_provisioning":
case "ignore_ssl_errors":
case "ignore_ssl_error":
$settings[$row["key"]] = boolval($row["value"]);
break;
default:
@@ -2449,7 +2461,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
if ($settings["authsource"] == "ldap"){
$settings['use_ssl'] = !isset($settings['use_ssl']) ? false : $settings['use_ssl'];
$settings['use_tls'] = !isset($settings['use_tls']) ? false : $settings['use_tls'];
$settings['ignore_ssl_errors'] = !isset($settings['ignore_ssl_errors']) ? false : $settings['ignore_ssl_errors'];
$settings['ignore_ssl_error'] = !isset($settings['ignore_ssl_error']) ? false : $settings['ignore_ssl_error'];
}
return $settings;
break;
@@ -3111,7 +3123,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(
@@ -3162,6 +3184,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
@@ -2535,6 +2535,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(
@@ -2706,6 +2710,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(
@@ -3188,6 +3196,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) {
}
}
@@ -4011,6 +4023,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
@@ -4964,6 +4979,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;
@@ -4977,10 +4995,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);
@@ -5018,7 +5040,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`
@@ -5958,6 +5989,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,
));
}
}
}
@@ -6010,6 +6047,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,
));
}
}
}