1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-02-22 12:18:48 +00:00

Merge branch 'staging' into nightly

This commit is contained in:
DerLinkman
2025-08-28 10:21:38 +02:00
24 changed files with 703 additions and 20 deletions

View File

@@ -71,6 +71,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
// Init records array
$spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />';
$dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
$mtasts_report_link = '<a href="https://mxtoolbox.com/dmarc/smtp-tls/how-to-setup-smtp-tls-reports" target="_blank">TLS Report Record Syntax</a>';
$records = array();
@@ -128,6 +129,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
);
}
$mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) {
$records[] = array(
'mta-sts.' . $domain,
'CNAME',
$mailcow_hostname
);
}
$records[] = array(
'_mta-sts.' . $domain,
'TXT',
"v={$mta_sts['version']};id={$mta_sts['id']};",
);
$records[] = array(
'_smtp._tls.' . $domain,
'TXT',
$mtasts_report_link,
);
}
$records[] = array(
$domain,
'TXT',
@@ -341,15 +363,25 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
}
foreach ($currents as &$current) {
if ($current['type'] == "TXT" &&
stripos(strtolower($current['txt']), 'v=sts') === 0) {
if (strtolower($current[$data_field[$current['type']]]) == strtolower($record[2])) {
$state = state_good;
}
else {
$state = state_nomatch;
}
$state .= '<br />' . $current[$data_field[$current['type']]];
}
if ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=dmarc') === 0 &&
$record[2] == $dmarc_link) {
stripos($current['txt'], 'v=dmarc') === 0 &&
$record[2] == $dmarc_link) {
$current['txt'] = str_replace(' ', '', $current['txt']);
$state = $current[$data_field[$current['type']]] . state_optional;
}
elseif ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=spf') === 0 &&
$record[2] == $spf_link) {
stripos($current['txt'], 'v=spf') === 0 &&
$record[2] == $spf_link) {
$state = state_nomatch;
$rslt = get_spf_allowed_hosts($record[0], true);
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
@@ -358,8 +390,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
}
elseif ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=dkim') === 0 &&
stripos($record[2], 'v=dkim') === 0) {
stripos($current['txt'], 'v=dkim') === 0 &&
stripos($record[2], 'v=dkim') === 0) {
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
@@ -367,7 +399,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
}
}
elseif ($current['type'] != 'TXT' &&
isset($data_field[$current['type']]) && $state != state_good) {
isset($data_field[$current['type']]) && $state != state_good) {
$state = state_nomatch;
if ($current[$data_field[$current['type']]] == $record[2]) {
$state = state_good;

View File

@@ -1401,6 +1401,80 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return mailbox('add', 'mailbox', $mailbox_attributes);
break;
case 'mta_sts':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$version = strtolower($_data['version']);
$mode = strtolower($_data['mode']);
$mx = explode(",", preg_replace('/\s+/', '', $_data['mx']));
$max_age = intval($_data['max_age']);
$active = (intval($_data['active']) == 1) ? 1 : 0;
$id = date('YmdHis');
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (empty($version) || !in_array($version, array('stsv1'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('version_invalid', htmlspecialchars($domain))
);
return false;
}
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mode_invalid', htmlspecialchars($domain))
);
return false;
}
if (empty($max_age) || $max_age < 0 || $max_age > 31536000) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('max_age_invalid', htmlspecialchars($domain))
);
return false;
}
foreach ($mx as $index => $mx_domain) {
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($mx_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mx_invalid', htmlspecialchars($mx_domain))
);
return false;
}
}
try {
$stmt = $pdo->prepare("INSERT INTO `mta_sts` (`id`, `domain`, `version`, `mode`, `mx`, `max_age`, `active`)
VALUES (:id, :domain, :version, :mode, :mx, :max_age, :active)");
$stmt->execute(array(
':id' => $id,
':domain' => $domain,
':version' => $version,
':mode' => $mode,
':mx' => implode(",", $mx),
':max_age' => $max_age,
':active' => $active
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data),
'msg' => $e->getMessage()
);
return false;
}
break;
case 'resource':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description'];
@@ -3742,6 +3816,125 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return true;
break;
case 'mta_sts':
if (!is_array($_data['domains'])) {
$domains = array();
$domains[] = $_data['domains'];
}
else {
$domains = $_data['domains'];
}
foreach ($domains as $domain) {
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => 'access_denied'
);
continue;
}
$is_now = mailbox('get', 'mta_sts', $domain);
if (!empty($is_now)) {
$version = (isset($_data['version'])) ? strtolower($_data['version']) : $is_now['version'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$active = ($active == 1) ? 1 : 0;
$mode = (isset($_data['mode'])) ? strtolower($_data['mode']) : $is_now['mode'];
$mx = (isset($_data['mx'])) ? explode(",", preg_replace('/\s+/', '', $_data['mx'])) : $is_now['mx'];
$max_age = (isset($_data['max_age'])) ? intval($_data['max_age']) : $is_now['max_age'];
// Update ID if neccesary
if ($version != strtolower($is_now['version']) ||
$mode != strtolower($is_now['mode']) ||
$mx != $is_now['mx'] ||
$max_age != $is_now['max_age']) {
$id = date('YmdHis');
} else {
$id = $is_now['id'];
}
} else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (empty($version) || !in_array($version, array('stsv1'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('version_invalid', htmlspecialchars($version))
);
continue;
}
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mode_invalid', htmlspecialchars($domain))
);
continue;
}
if (empty($max_age) || $max_age < 0 || $max_age > 31557600) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('max_age_invalid', htmlspecialchars($domain))
);
continue;
}
foreach ($mx as $index => $mx_domain) {
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
$invalid_mx = false;
if (!is_valid_domain_name($mx_domain)) {
$invalid_mx = $mx_domain;
break;
}
}
if ($invalid_mx) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mx_invalid', htmlspecialchars($invalid_mx))
);
continue;
}
try {
$stmt = $pdo->prepare("UPDATE `mta_sts` SET `id` = :id, `version` = :version, `mode` = :mode, `mx` = :mx, `max_age` = :max_age, `active` = :active WHERE `domain` = :domain");
$stmt->execute(array(
':id' => $id,
':domain' => $domain,
':version' => $version,
':mode' => $mode,
':mx' => implode(",", $mx),
':max_age' => $max_age,
':active' => $active
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data),
'msg' => $e->getMessage()
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('object_modified', $domain)
);
}
return true;
break;
case 'resource':
if (!is_array($_data['name'])) {
$names = array();
@@ -5030,6 +5223,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return $rows;
}
break;
case 'mta_sts':
$stmt = $pdo->prepare("SELECT * FROM `mta_sts` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)){
return [];
}
$row['mx'] = explode(',', $row['mx']);
$row['version'] = strtoupper(substr($row['version'], 0, 3)) . substr($row['version'], 3);
return $row;
break;
case 'resource_details':
$resourcedata = array();
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
@@ -5415,6 +5622,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `mta_sts` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);");
$stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);");
try {

View File

@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "06082025_1611";
$db_version = "19082025_1436";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -475,6 +475,23 @@ function init_db_schema()
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"mta_sts" => array(
"cols" => array(
"id" => "BIGINT NOT NULL",
"domain" => "VARCHAR(255) NOT NULL",
"version" => "VARCHAR(255) NOT NULL",
"mode" => "VARCHAR(255) NOT NULL",
"mx" => "VARCHAR(255) NOT NULL",
"max_age" => "VARCHAR(255) NOT NULL",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
"primary" => array(
"" => array("domain")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"user_acl" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",