git subrepo commit (merge) mailcow/src/mailcow-dockerized
subrepo:
subdir: "mailcow/src/mailcow-dockerized"
merged: "81fb18894"
upstream:
origin: "https://github.com/mailcow/mailcow-dockerized.git"
branch: "master"
commit: "4ca02ecee"
git-subrepo:
version: "0.4.6"
origin: "???"
commit: "???"
Update to upstream 2026-10. Local patches re-applied by hand
(no Unbound DNS for containers, IPv6 off by default, fake-MySQL
stubs in dockerapi, Postfix overrides, no .env symlink); dropped
the unused dovecot and clamd Dockerfile patches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Change-Id: I2c2c194d1d436b841358685f5f94aef09eb19ae8
diff --git a/mailcow/src/mailcow-dockerized/data/web/json_api.php b/mailcow/src/mailcow-dockerized/data/web/json_api.php
index 28f8cac..fc34b25 100644
--- a/mailcow/src/mailcow-dockerized/data/web/json_api.php
+++ b/mailcow/src/mailcow-dockerized/data/web/json_api.php
@@ -47,6 +47,12 @@
}
}
+// Block requests not intended for direct API use by checking the 'Sec-Fetch-Dest' header.
+if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] !== 'empty') {
+ header('HTTP/1.1 403 Forbidden');
+ exit;
+}
+
if (isset($_GET['query'])) {
$query = explode('/', $_GET['query']);
@@ -85,6 +91,11 @@
if ($action == 'delete') {
$_POST['items'] = $request;
}
+
+ // search
+ if ($action == 'search') {
+ // placeholder for search, as the request body is already decoded and available in $requestDecoded
+ }
}
api_log($_POST);
@@ -163,6 +174,8 @@
exit;
}
fido2(array("action" => "register", "registration" => $data));
+ // Release pending_tfa_setup session hold
+ unset($_SESSION['pending_tfa_setup']);
$return = new stdClass();
$return->success = true;
echo json_encode($return);
@@ -318,6 +331,9 @@
case "app-passwd":
process_add_return(app_passwd('add', $attr));
break;
+ case "mta-sts":
+ process_add_return(mailbox('add', 'mta_sts', $attr));
+ break;
// return no route found if no case is matched
default:
http_response_code(404);
@@ -328,81 +344,6 @@
exit();
}
break;
- case "process":
- // only allow POST requests to process API endpoints
- if ($_SERVER['REQUEST_METHOD'] != 'POST') {
- http_response_code(405);
- echo json_encode(array(
- 'type' => 'error',
- 'msg' => 'only POST method is allowed'
- ));
- exit();
- }
- switch ($category) {
- case "fido2-args":
- header('Content-Type: application/json');
- $post = trim(file_get_contents('php://input'));
- if ($post) {
- $post = json_decode($post);
- }
- $clientDataJSON = base64_decode($post->clientDataJSON);
- $authenticatorData = base64_decode($post->authenticatorData);
- $signature = base64_decode($post->signature);
- $id = base64_decode($post->id);
- $challenge = $_SESSION['challenge'];
- $process_fido2 = fido2(array("action" => "get_by_b64cid", "cid" => $post->id));
- if ($process_fido2['pub_key'] === false) {
- $return = new stdClass();
- $return->success = false;
- echo json_encode($return);
- exit;
- }
- try {
- $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_fido2['pub_key'], $challenge, null, $GLOBALS['FIDO2_UV_FLAG_LOGIN'], $GLOBALS['FIDO2_USER_PRESENT_FLAG']);
- }
- catch (Throwable $ex) {
- unset($process_fido2);
- $return = new stdClass();
- $return->success = false;
- echo json_encode($return);
- exit;
- }
- $return = new stdClass();
- $return->success = true;
- $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
- $stmt->execute(array(':username' => $process_fido2['username']));
- $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($obj_props['superadmin'] === 1) {
- $_SESSION["mailcow_cc_role"] = "admin";
- }
- elseif ($obj_props['superadmin'] === 0) {
- $_SESSION["mailcow_cc_role"] = "domainadmin";
- }
- else {
- $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
- $stmt->execute(array(':username' => $process_fido2['username']));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($row['username'] == $process_fido2['username']) {
- $_SESSION["mailcow_cc_role"] = "user";
- }
- }
- if (empty($_SESSION["mailcow_cc_role"])) {
- session_unset();
- session_destroy();
- exit;
- }
- $_SESSION["mailcow_cc_username"] = $process_fido2['username'];
- $_SESSION["fido2_cid"] = $process_fido2['cid'];
- unset($_SESSION["challenge"]);
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array("fido2_login"),
- 'msg' => array('logged_in_as', $process_fido2['username'])
- );
- echo json_encode($return);
- break;
- }
- break;
case "get":
function process_get_return($data, $object = true) {
if ($object === true) {
@@ -503,16 +444,6 @@
print(json_encode($getArgs));
$_SESSION['challenge'] = $WebAuthn->getChallenge();
return;
- break;
- case "fail2ban":
- if (!isset($_SESSION['mailcow_cc_role'])){
- switch ($object) {
- case 'banlist':
- header('Content-Type: text/plain');
- echo fail2ban('banlist', 'get', $extra);
- break;
- }
- }
break;
}
if (isset($_SESSION['mailcow_cc_role'])) {
@@ -533,47 +464,6 @@
case "domain":
switch ($object) {
- case "datatables":
- $table = ['domain', 'd'];
- $primaryKey = 'domain';
- $columns = [
- ['db' => 'domain', 'dt' => 2],
- ['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"],
- ['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
- ['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
- ['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"],
- ['db' => 'defquota', 'dt' => 7],
- ['db' => 'maxquota', 'dt' => 8],
- ['db' => 'backupmx', 'dt' => 10],
- ['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']],
- ['db' => 'active', 'dt' => 15],
- ];
-
- require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
- global $pdo;
- if($_SESSION['mailcow_cc_role'] === 'admin') {
- $data = SSP::simple($_GET, $pdo, $table, $primaryKey, $columns);
- } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
- $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
- 'INNER JOIN domain_admins as da ON da.domain = d.domain',
- [
- 'condition' => 'da.active = 1 and da.username = :username',
- 'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
- ]);
- }
-
- if (!empty($data['data'])) {
- $domainsData = [];
- foreach ($data['data'] as $domain) {
- if ($details = mailbox('get', 'domain_details', $domain[2])) {
- $domainsData[] = $details;
- }
- }
- $data['data'] = $domainsData;
- }
-
- process_get_return($data);
- break;
case "all":
$tags = null;
if (isset($_GET['tags']) && $_GET['tags'] != '')
@@ -947,6 +837,17 @@
}
echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
break;
+ case "cron":
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
+ $logs = get_logs('cron-mailcow', $extra);
+ }
+ else {
+ $logs = get_logs('cron-mailcow');
+ }
+ echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
+ break;
case "postfix":
// 0 is first record, so empty is fine
if (isset($extra)) {
@@ -1062,45 +963,6 @@
break;
case "mailbox":
switch ($object) {
- case "datatables":
- $table = ['mailbox', 'm'];
- $primaryKey = 'username';
- $columns = [
- ['db' => 'username', 'dt' => 2],
- ['db' => 'quota', 'dt' => 3],
- ['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"],
- ['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
- ['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
- ['db' => 'messages', 'dt' => 17, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
- ['db' => 'tags', 'dt' => 20, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
- ['db' => 'active', 'dt' => 21]
- ];
-
- require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
- global $pdo;
- if($_SESSION['mailcow_cc_role'] === 'admin') {
- $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, null, "(`m`.`kind` = '' OR `m`.`kind` = NULL)");
- } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
- $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
- 'INNER JOIN domain_admins as da ON da.domain = m.domain',
- [
- 'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username",
- 'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
- ]);
- }
-
- if (!empty($data['data'])) {
- $mailboxData = [];
- foreach ($data['data'] as $mailbox) {
- if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) {
- $mailboxData[] = $details;
- }
- }
- $data['data'] = $mailboxData;
- }
-
- process_get_return($data);
- break;
case "all":
case "reduced":
$tags = null;
@@ -1414,10 +1276,6 @@
break;
case "fail2ban":
switch ($object) {
- case 'banlist':
- header('Content-Type: text/plain');
- echo fail2ban('banlist', 'get', $extra);
- break;
default:
$data = fail2ban('get');
process_get_return($data);
@@ -1635,23 +1493,6 @@
);
echo json_encode($temp, JSON_UNESCAPED_SLASHES);
break;
- case "solr":
- $solr_status = solr_status();
- $solr_size = ($solr_status['status']['dovecot-fts']['index']['size']);
- $solr_documents = ($solr_status['status']['dovecot-fts']['index']['numDocs']);
- if (strtolower(getenv('SKIP_SOLR')) != 'n') {
- $solr_enabled = false;
- }
- else {
- $solr_enabled = true;
- }
- echo json_encode(array(
- 'type' => 'info',
- 'solr_enabled' => $solr_enabled,
- 'solr_size' => $solr_size,
- 'solr_documents' => $solr_documents
- ));
- break;
case "host":
if (!$extra){
$stats = docker("host_stats");
@@ -1691,6 +1532,13 @@
$score = array("score" => preg_replace("/\s+/", "", $score));
process_get_return($score);
break;
+ case "identity-provider":
+ if($_SESSION['mailcow_cc_role'] === 'admin') {
+ process_get_return($iam_settings);
+ } else {
+ process_get_return(null);
+ }
+ break;
break;
// return no route found if no case is matched
default:
@@ -1703,6 +1551,136 @@
}
}
break;
+ case "search":
+ function process_search_return($return) {
+ if ($return === false) {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Cannot get item'
+ ));
+ }
+ else {
+ echo json_encode($return, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ }
+ }
+ // only allow POST requests to SEARCH API endpoints
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ http_response_code(405);
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'only POST method is allowed'
+ ));
+ exit();
+ }
+
+ // Load SSP class
+ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
+ global $pdo;
+
+ switch ($category) {
+ case "domain":
+ $table = ['domain', 'd'];
+ $primaryKey = 'domain';
+ $columns = [
+ ['db' => 'domain', 'dt' => 2],
+ ['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"],
+ ['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
+ ['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
+ ['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"],
+ ['db' => 'defquota', 'dt' => 7],
+ ['db' => 'maxquota', 'dt' => 8],
+ ['db' => 'backupmx', 'dt' => 10],
+ ['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']],
+ ['db' => 'active', 'dt' => 15],
+ ];
+
+ if($_SESSION['mailcow_cc_role'] === 'admin') {
+ $data = SSP::simple($requestDecoded, $pdo, $table, $primaryKey, $columns);
+ } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
+ $data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns,
+ 'INNER JOIN domain_admins as da ON da.domain = d.domain',
+ [
+ 'condition' => 'da.active = 1 and da.username = :username',
+ 'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
+ ]);
+ } else {
+ http_response_code(403);
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Insufficient permissions'
+ ));
+ exit();
+ }
+
+ if (!empty($data['data'])) {
+ $domainsData = [];
+ foreach ($data['data'] as $domain) {
+ if ($details = mailbox('get', 'domain_details', $domain[2])) {
+ $domainsData[] = $details;
+ }
+ }
+ $data['data'] = $domainsData;
+ }
+
+ process_search_return($data);
+ break;
+
+ case "mailbox":
+ $table = ['mailbox', 'm'];
+ $primaryKey = 'username';
+ $columns = [
+ ['db' => 'username', 'dt' => 2],
+ ['db' => 'quota', 'dt' => 3],
+ ['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"],
+ ['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
+ ['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
+ ['db' => 'name', 'dt' => 7],
+ ['db' => 'messages', 'dt' => 20, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
+ ['db' => 'tags', 'dt' => 23, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
+ ['db' => 'active', 'dt' => 24],
+ ];
+
+ if($_SESSION['mailcow_cc_role'] === 'admin') {
+ $data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns, null,
+ "(`m`.`kind` = '' OR `m`.`kind` = NULL)");
+ } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
+ $data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns,
+ 'INNER JOIN domain_admins as da ON da.domain = m.domain',
+ [
+ 'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username",
+ 'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
+ ]);
+ } else {
+ http_response_code(403);
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Insufficient permissions'
+ ));
+ exit();
+ }
+
+ if (!empty($data['data'])) {
+ $mailboxData = [];
+ foreach ($data['data'] as $mailbox) {
+ if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) {
+ $mailboxData[] = $details;
+ }
+ }
+ $data['data'] = $mailboxData;
+ }
+
+ process_search_return($data);
+ break;
+
+ default:
+ http_response_code(404);
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Invalid search category'
+ ));
+ break;
+ }
+ break;
case "delete":
if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username']) || !isset($_SESSION["mailcow_cc_username"])) {
http_response_code(403);
@@ -1844,6 +1822,9 @@
case "rlhash":
echo ratelimit('delete', null, implode($items));
break;
+ case "identity-provider":
+ process_delete_return(identity_provider('delete'));
+ break;
// return no route found if no case is matched
default:
http_response_code(404);
@@ -1967,7 +1948,6 @@
case "quota_notification_bcc":
process_edit_return(quota_notification_bcc('edit', $attr));
break;
- break;
case "mailq":
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
break;
@@ -2015,6 +1995,9 @@
case "rl-mbox":
process_edit_return(ratelimit('edit', 'mailbox', array_merge(array('object' => $items), $attr)));
break;
+ case "rename-mbox":
+ process_edit_return(mailbox('edit', 'mailbox_rename', array_merge(array('mailbox' => $items), $attr)));
+ break;
case "user-acl":
process_edit_return(acl('edit', 'user', array_merge(array('username' => $items), $attr)));
break;
@@ -2052,6 +2035,12 @@
case "ip_check":
process_edit_return(customize('edit', 'ip_check', $attr));
break;
+ case "custom_login":
+ process_edit_return(customize('edit', 'custom_login', $attr));
+ break;
+ case "ui_theme":
+ process_edit_return(customize('edit', 'ui_theme', $attr));
+ break;
case "self":
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
process_edit_return(domain_admin('edit', $attr));
@@ -2063,6 +2052,21 @@
case "cors":
process_edit_return(cors('edit', $attr));
break;
+ case "identity-provider":
+ process_edit_return(identity_provider('edit', $attr));
+ break;
+ case "identity-provider-test":
+ process_edit_return(identity_provider('test', $attr));
+ break;
+ case "cors":
+ process_edit_return(cors('edit', $attr));
+ break;
+ case "reset-password-notification":
+ process_edit_return(reset_password('edit_notification', $attr));
+ break;
+ case "mta-sts":
+ process_edit_return(mailbox('edit', 'mta_sts', array_merge(array('domains' => $items), $attr)));
+ break;
// return no route found if no case is matched
default:
http_response_code(404);