mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2025-12-12 17:36:01 +00:00
pf/php: add mta-sts support (outbound) (#6686)
* added mta-sts-resolver into postfix config + daemon * [Web] Add MTA-STS support * [Web] Fix mta-sts server_name * updated .gitignore * [ACME] fetch cert for mta-sts subdomain * [Web] change MTA-STS id to human-readable timestamp * [Web] Remove MTA-STS version STSv2 * [Web] Fix MTA-STS DNS check * [Web] add max_age limit for MTA-STS policy * Added tooltips and info texts to mta-sts webui page * postfix: replace mta-sts-resolver with postfix-tlspol --------- Co-authored-by: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -75,3 +75,4 @@ refresh_images.sh
|
||||
update_diffs/
|
||||
create_cold_standby.sh
|
||||
!data/conf/nginx/mailcow_auth.conf
|
||||
data/conf/postfix/mta-sts-resolver
|
||||
@@ -206,7 +206,7 @@ while true; do
|
||||
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
# Fetch certs for autoconfig and autodiscover subdomains
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
|
||||
fi
|
||||
|
||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on
|
||||
|
||||
RUN git clone https://github.com/Zuplu/postfix-tlspol.git && \
|
||||
cd postfix-tlspol && \
|
||||
scripts/build.sh build-only
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL C
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
||||
&& ln -sf /bin/true /sbin/initctl \
|
||||
@@ -48,6 +58,7 @@ COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
|
||||
COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY --from=builder /src/postfix-tlspol/build/postfix-tlspol /usr/local/bin/postfix-tlspol
|
||||
|
||||
RUN chmod +x /opt/postfix.sh \
|
||||
/usr/local/bin/rspamd-pipe-ham \
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
trap "postfix stop" EXIT
|
||||
|
||||
[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
|
||||
[[ ! -d /etc/postfix-tlspol ]] && mkdir -p /etc/postfix-tlspol
|
||||
[[ ! -d /var/lib/postfix-tlspol ]] && mkdir -p /var/lib/postfix-tlspol
|
||||
|
||||
# Wait for MySQL to warm-up
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
@@ -503,6 +505,26 @@ if [[ ! -f /opt/postfix/conf/custom_postscreen_whitelist.cidr ]]; then
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/postfix-tlspol/config.yaml
|
||||
server:
|
||||
address: 127.0.0.1:8642
|
||||
|
||||
log-level: info
|
||||
|
||||
prefetch: true
|
||||
|
||||
cache-file: /var/lib/postfix-tlspol/cache.db
|
||||
|
||||
dns:
|
||||
# must support DNSSEC
|
||||
address: 127.0.0.11:53
|
||||
EOF
|
||||
|
||||
# Fixing local command execution of postfix-tlspol with symlink to config
|
||||
if [ ! -L /etc/postfix-tlspol/config.yaml ]; then
|
||||
ln -s /opt/postfix/conf/postfix-tlspol/config.yaml /etc/postfix-tlspol/config.yaml
|
||||
fi
|
||||
|
||||
# Fix Postfix permissions
|
||||
chown -R root:postfix /opt/postfix/conf/sql/ /opt/postfix/conf/custom_transport.pcre
|
||||
chmod 640 /opt/postfix/conf/sql/*.cf /opt/postfix/conf/custom_transport.pcre
|
||||
@@ -524,4 +546,4 @@ if [[ $? != 0 ]]; then
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
fi
|
||||
@@ -11,6 +11,15 @@ stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
|
||||
[program:postfix-tlspol]
|
||||
startsecs=10
|
||||
autorestart=true
|
||||
command=/usr/local/bin/postfix-tlspol -config /opt/postfix/conf/postfix-tlspol/config.yaml
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:postfix]
|
||||
command=/opt/postfix.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
|
||||
@@ -48,13 +48,21 @@ http {
|
||||
listen {{ HTTP_PORT }} default_server;
|
||||
listen [::]:{{ HTTP_PORT }} default_server;
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
|
||||
if ( $request_uri ~* "%0A|%0D" ) { return 403; }
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
default_type "text/plain";
|
||||
}
|
||||
location ^~ /.well-known/mta-sts.txt {
|
||||
allow all;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$uri$is_args$args;
|
||||
}
|
||||
@@ -82,7 +90,7 @@ http {
|
||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/mail/key.pem;
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*;
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*;
|
||||
|
||||
include /etc/nginx/includes/sites-default.conf;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
default_type "text/plain";
|
||||
}
|
||||
location ^~ /.well-known/mta-sts.txt {
|
||||
allow all;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
|
||||
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;
|
||||
|
||||
@@ -152,7 +152,7 @@ smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||
smtp_sasl_security_options =
|
||||
smtp_sasl_mechanism_filter = plain, login
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:127.0.0.1:8642:QUERY
|
||||
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||
mail_name = Postcow
|
||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||
|
||||
@@ -48,6 +48,12 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
$rl = ratelimit('get', 'domain', $domain);
|
||||
$rlyhosts = relayhost('get');
|
||||
$domain_footer = mailbox('get', 'domain_wide_footer', $domain);
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
if (count($mta_sts) == 0) {
|
||||
$mta_sts = false;
|
||||
} elseif (isset($mta_sts['mx'])) {
|
||||
$mta_sts['mx'] = implode(',', $mta_sts['mx']);
|
||||
}
|
||||
$template = 'edit/domain.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
@@ -58,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'dkim' => dkim('details', $domain),
|
||||
'domain_details' => $result,
|
||||
'domain_footer' => $domain_footer,
|
||||
'mta_sts' => $mta_sts,
|
||||
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
||||
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1400,6 +1400,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'];
|
||||
@@ -3741,6 +3815,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();
|
||||
@@ -5029,6 +5222,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)) {
|
||||
@@ -5414,6 +5621,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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -324,6 +324,9 @@ if (isset($_GET['query'])) {
|
||||
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);
|
||||
@@ -2001,6 +2004,9 @@ if (isset($_GET['query'])) {
|
||||
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);
|
||||
|
||||
@@ -482,10 +482,13 @@
|
||||
"mailboxes_in_use": "Maximale Anzahl an Mailboxen muss größer oder gleich %d sein",
|
||||
"malformed_username": "Benutzername hat ein falsches Format",
|
||||
"map_content_empty": "Inhalt darf nicht leer sein",
|
||||
"max_age_invalid": "Maximales Alter %s ist ungültig",
|
||||
"max_alias_exceeded": "Anzahl an Alias-Adressen überschritten",
|
||||
"max_mailbox_exceeded": "Anzahl an Mailboxen überschritten (%d von %d)",
|
||||
"max_quota_in_use": "Mailbox-Speicherplatzlimit muss größer oder gleich %d MiB sein",
|
||||
"maxquota_empty": "Max. Speicherplatz pro Mailbox darf nicht 0 sein.",
|
||||
"mode_invalid": "Modus %s ist ungültig",
|
||||
"mx_invalid": "MX-Eintrag %s ist ungültig",
|
||||
"mysql_error": "MySQL-Fehler: %s",
|
||||
"network_host_invalid": "Netzwerk oder Host ungültig: %s",
|
||||
"next_hop_interferes": "%s verhindert das Hinzufügen von Next Hop %s",
|
||||
@@ -545,6 +548,7 @@
|
||||
"username_invalid": "Benutzername %s kann nicht verwendet werden",
|
||||
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
|
||||
"value_missing": "Bitte alle Felder ausfüllen",
|
||||
"version_invalid": "Version %s ist ungültig",
|
||||
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
|
||||
"template_exists": "Vorlage %s existiert bereits",
|
||||
"template_id_invalid": "Vorlagen-ID %s ungültig",
|
||||
@@ -703,6 +707,17 @@
|
||||
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
|
||||
"mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.",
|
||||
"mins_interval": "Intervall (min)",
|
||||
"mta_sts": "MTA-STS",
|
||||
"mta_sts_info": "<a href='https://de.wikipedia.org/wiki/STARTTLS#MTA-STS' target='_blank'>MTA-STS</a> ist ein Standard, der den E-Mail-Versand zwischen Mailservern zwingt, TLS mit gültigen Zertifikaten zu verwenden. <br>Er wird verwendet, wenn <a target='_blank' href='https://de.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> aufgrund fehlender oder nicht unterstützter DNSSEC nicht möglich ist.<br><b>Hinweis</b>: Wenn die empfangende Domain DANE mit DNSSEC unterstützt, wird DANE <b>immer</b> bevorzugt – MTA-STS fungiert nur als Fallback.",
|
||||
"mta_sts_version": "Version",
|
||||
"mta_sts_version_info": "Definiert die Version des MTA-STS-Standards – derzeit ist nur <code>STSv1</code> gültig.",
|
||||
"mta_sts_mode": "Modus",
|
||||
"mta_sts_mode_info": "Es gibt drei Modi zur Auswahl:<ul><li><em>testing</em> – Die Richtlinie wird nur überwacht, Verstöße haben keine Auswirkungen.</li><li><em>enforce</em> – Die Richtlinie wird strikt durchgesetzt, Verbindungen ohne gültiges TLS werden abgelehnt.</li><li><em>none</em> – Die Richtlinie wird veröffentlicht, aber nicht angewendet.</li></ul>",
|
||||
"mta_sts_max_age": "Maximales Alter",
|
||||
"mta_sts_max_age_info": "Zeit in Sekunden, die empfangende Mailserver diese Richtlinie zwischenspeichern dürfen, bevor sie erneut abgerufen wird.",
|
||||
"mta_sts_mx": "MX-Server",
|
||||
"mta_sts_mx_info": "Erlaubt das Senden nur an explizit aufgeführte Mailserver-Hostnamen; der sendende MTA überprüft, ob der DNS-MX-Hostname mit der Richtlinienliste übereinstimmt, und erlaubt die Zustellung nur mit einem gültigen TLS-Zertifikat (schützt vor MITM).",
|
||||
"mta_sts_mx_notice": "Es können mehrere MX-Server angegeben werden (durch Kommas getrennt).",
|
||||
"multiple_bookings": "Mehrfaches Buchen",
|
||||
"nexthop": "Next Hop",
|
||||
"none_inherit": "Keine Auswahl / Erben",
|
||||
@@ -852,7 +867,7 @@
|
||||
"add_tls_policy_map": "TLS-Richtlinieneintrag hinzufügen",
|
||||
"address_rewriting": "Adressumschreibung",
|
||||
"alias": "Alias",
|
||||
"alias_domain_alias_hint": "Alias-Adressen werden <b>nicht</b> automatisch auch auf Domain-Alias Adressen angewendet. Eine Alias-Adresse <code>mein-alias@domain</code> bildet demnach <b>nicht</b> die Adresse <code>mein-alias@alias-domain</code> ab.<br>E-Mail-Weiterleitungen an externe Postfächer sollten über Sieve (SOGo Weiterleitung oder im Reiter \"Filter\") angelegt werden. Der Button \"Alias über Alias-Domains expandieren\" erstellt fehlende Alias-Adressen in Alias-Domains.",
|
||||
"alias_domain_alias_hint": "Alias-Adressen werden <b>nicht</b> automatisch auch auf Domain-Alias Adressen angewendet. Eine Alias-Adresse <code>mein-alias@domain</code> bildet demnach <b>nicht</b> die Adresse <code>mein-alias@alias-domain</code> ab.<br>E-Mail-Weiterleitungen an externe Postfächer sollten über Sieve (SOGo Weiterleitung oder im Reiter Filter) angelegt werden. Der Button Alias über Alias-Domains expandieren erstellt fehlende Alias-Adressen in Alias-Domains.",
|
||||
"alias_domain_backupmx": "Alias-Domain für Relay-Domain inaktiv",
|
||||
"aliases": "Aliasse",
|
||||
"allow_from_smtp": "Nur folgende IPs für <b>SMTP</b> erlauben",
|
||||
@@ -1248,7 +1263,7 @@
|
||||
"delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?",
|
||||
"direct_aliases": "Direkte Alias-Adressen",
|
||||
"direct_aliases_desc": "Nur direkte Alias-Adressen werden für benutzerdefinierte Einstellungen berücksichtigt.",
|
||||
"direct_protocol_access": "Der Hauptbenutzer hat <b>direkten, externen Zugriff</b> auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.<br>Der Button \"Webmail\" kann unabhängig der Einstellung immer verwendet werden.",
|
||||
"direct_protocol_access": "Der Hauptbenutzer hat <b>direkten, externen Zugriff</b> auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.<br>Der Button Webmail kann unabhängig der Einstellung immer verwendet werden.",
|
||||
"eas_reset": "ActiveSync-Geräte-Cache zurücksetzen",
|
||||
"eas_reset_help": "In vielen Fällen kann ein ActiveSync-Profil durch das Zurücksetzen des Caches repariert werden.<br><b>Vorsicht:</b> Alle Elemente werden erneut heruntergeladen!",
|
||||
"eas_reset_now": "Jetzt zurücksetzen",
|
||||
|
||||
@@ -483,10 +483,13 @@
|
||||
"mailboxes_in_use": "Max. mailboxes must be greater or equal to %d",
|
||||
"malformed_username": "Malformed username",
|
||||
"map_content_empty": "Map content cannot be empty",
|
||||
"max_age_invalid": "Max age %s is invalid",
|
||||
"max_alias_exceeded": "Max. aliases exceeded",
|
||||
"max_mailbox_exceeded": "Max. mailboxes exceeded (%d of %d)",
|
||||
"max_quota_in_use": "Mailbox quota must be greater or equal to %d MiB",
|
||||
"maxquota_empty": "Max. quota per mailbox must not be 0.",
|
||||
"mode_invalid": "Mode %s is invalid",
|
||||
"mx_invalid": "MX record %s is invalid",
|
||||
"mysql_error": "MySQL error: %s",
|
||||
"network_host_invalid": "Invalid network or host: %s",
|
||||
"next_hop_interferes": "%s interferes with nexthop %s",
|
||||
@@ -550,6 +553,7 @@
|
||||
"username_invalid": "Username %s cannot be used",
|
||||
"validity_missing": "Please assign a period of validity",
|
||||
"value_missing": "Please provide all values",
|
||||
"version_invalid": "Version %s is invalid",
|
||||
"yotp_verification_failed": "Yubico OTP verification failed: %s"
|
||||
},
|
||||
"datatables": {
|
||||
@@ -704,6 +708,17 @@
|
||||
"maxbytespersecond": "Max. bytes per second <br><small>(0 = unlimited)</small>",
|
||||
"mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.",
|
||||
"mins_interval": "Interval (min)",
|
||||
"mta_sts": "MTA-STS",
|
||||
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> is a standard that enforces email delivery between mail servers to use TLS with valid certificates. <br>It is used when <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> is not possible due to missing or unsupported DNSSEC.<br><b>Note</b>: If the receiving domain supports DANE with DNSSEC, DANE is <b>always</b> preferred – MTA-STS only acts as a fallback.",
|
||||
"mta_sts_version": "Version",
|
||||
"mta_sts_version_info": "Defines the version of the MTA-STS standard – currently only <code>STSv1</code> is valid." ,
|
||||
"mta_sts_mode": "Mode",
|
||||
"mta_sts_mode_info": "There are three modes to choose from:<ul><li><em>testing</em> – policy is only monitored, violations have no impact.</li><li><em>enforce</em> – policy is strictly enforced, connections without valid TLS are rejected.</li><li><em>none</em> – policy is published but not applied.</li></ul>",
|
||||
"mta_sts_max_age": "Max age",
|
||||
"mta_sts_max_age_info": "Time in seconds that receiving mail servers may cache this policy until refetching.",
|
||||
"mta_sts_mx": "MX server",
|
||||
"mta_sts_mx_info": "Allows sending only to explicitly listed mail server hostnames; the sending MTA checks if the DNS MX hostname matches the policy list, and only allows delivery with a valid TLS certificate (guards against MITM).",
|
||||
"mta_sts_mx_notice": "Multiple MX servers can be specified (separated by commas).",
|
||||
"multiple_bookings": "Multiple bookings",
|
||||
"none_inherit": "None / Inherit",
|
||||
"nexthop": "Next hop",
|
||||
|
||||
30
data/web/mta-sts.php
Normal file
30
data/web/mta-sts.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
if (!isset($_SERVER['HTTP_HOST']) || strpos($_SERVER['HTTP_HOST'], 'mta-sts.') !== 0) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$domain = str_replace('mta-sts.', '', $_SERVER['HTTP_HOST']);
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
|
||||
if (count($mta_sts) == 0 ||
|
||||
!isset($mta_sts['version']) ||
|
||||
!isset($mta_sts['mode']) ||
|
||||
!isset($mta_sts['max_age']) ||
|
||||
!isset($mta_sts['mx']) ||
|
||||
$mta_sts['active'] != 1) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo "version: {$mta_sts['version']}\n";
|
||||
echo "mode: {$mta_sts['mode']}\n";
|
||||
echo "max_age: {$mta_sts['max_age']}\n";
|
||||
foreach ($mta_sts['mx'] as $mx) {
|
||||
echo "mx: {$mx}\n";
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -8,6 +8,7 @@
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dratelimit">{{ lang.edit.ratelimit }}</button></li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dspamfilter">{{ lang.edit.spam_filter }}</button></li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dqwbcc">{{ lang.edit.quota_warning_bcc }}</button></li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dmtasts">{{ lang.edit.mta_sts }}</button></li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dfooter">{{ lang.edit.domain_footer }}</button></li>
|
||||
</ul>
|
||||
<hr class="d-none d-md-block">
|
||||
@@ -278,6 +279,82 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dmtasts" class="tab-pane fade" role="tabpanel" aria-labelledby="domain-mtasts">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex d-md-none fs-5">
|
||||
<button class="btn flex-grow-1 text-start" data-bs-target="#collapse-tab-mtasts" data-bs-toggle="collapse" aria-controls="collapse-tab-mtasts">
|
||||
{{ lang.edit.mta_sts }} <span class="badge bg-info table-lines"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="collapse-tab-mtasts" class="card-body collapse" data-bs-parent="#domain-content">
|
||||
<h4>{{ lang.edit.mta_sts }}</h4>
|
||||
<p>{{ lang.edit.mta_sts_info|raw }}</p>
|
||||
<form data-id="dommtasts" method="post">
|
||||
<input type="hidden" value="0" name="active">
|
||||
<input type="hidden" value="{{ domain }}" name="domain">
|
||||
<div class="row mb-2">
|
||||
<label class="control-label col-sm-2" for="version">
|
||||
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.edit.mta_sts_version_info|raw }}"></i>
|
||||
{{ lang.edit.mta_sts_version }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select data-style="btn btn-light" class="form-control" name="version" title="" required>
|
||||
<option value="stsv1"{% if mta_sts.version == 'STSv1' %} selected{% endif %}>STSv1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label class="control-label col-sm-2" for="mode">
|
||||
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.edit.mta_sts_mode_info|raw }}"></i>
|
||||
{{ lang.edit.mta_sts_mode }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select data-style="btn btn-light" class="form-control" name="mode" title="" required>
|
||||
<option value="enforce"{% if mta_sts.mode == 'enforce' %} selected{% endif %}>enforce</option>
|
||||
<option value="testing"{% if mta_sts.mode == 'testing' %} selected{% endif %}>testing</option>
|
||||
<option value="none"{% if mta_sts.mode == 'none' %} selected{% endif %}>none</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label class="control-label col-sm-2" for="max_age">
|
||||
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.edit.mta_sts_max_age_info|raw }}"></i>
|
||||
{{ lang.edit.mta_sts_max_age }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" name="max_age" value="{{ mta_sts.max_age }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label class="control-label col-sm-2" for="mx">
|
||||
<i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.edit.mta_sts_mx_info|raw }}"></i>
|
||||
{{ lang.edit.mta_sts_mx }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea autocorrect="off" autocapitalize="none" class="form-control" rows="5" name="mx">{{ mta_sts.mx }}</textarea>
|
||||
<small class="text-muted">{{ lang.edit.mta_sts_mx_notice|raw }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="active"{% if mta_sts.active == '1' %} checked{% endif %}> {{ lang.edit.active }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
{% if mta_sts == false %}
|
||||
<button class="btn btn-xs-lg d-block d-sm-inline btn-secondary" data-action="add_item" data-id="dommtasts" data-item="{{ domain }}" data-api-url='add/mta-sts' data-api-attr='{}' href="#">{{ lang.admin.save }}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-xs-lg d-block d-sm-inline btn-secondary" data-action="edit_selected" data-id="dommtasts" data-item="{{ domain }}" data-api-url='edit/mta-sts' data-api-attr='{}' href="#">{{ lang.admin.save }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dfooter" class="tab-pane fade" role="tabpanel" aria-labelledby="domain-footer">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex d-md-none fs-5">
|
||||
|
||||
@@ -338,7 +338,7 @@ services:
|
||||
- dovecot
|
||||
|
||||
postfix-mailcow:
|
||||
image: ghcr.io/mailcow/postfix:1.80
|
||||
image: ghcr.io/mailcow/postfix:1.81
|
||||
depends_on:
|
||||
mysql-mailcow:
|
||||
condition: service_started
|
||||
@@ -440,7 +440,7 @@ services:
|
||||
condition: service_started
|
||||
unbound-mailcow:
|
||||
condition: service_healthy
|
||||
image: ghcr.io/mailcow/acme:1.93
|
||||
image: ghcr.io/mailcow/acme:1.94
|
||||
dns:
|
||||
- ${IPV4_NETWORK:-172.22.1}.254
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user