mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2025-12-13 18:06: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/
|
update_diffs/
|
||||||
create_cold_standby.sh
|
create_cold_standby.sh
|
||||||
!data/conf/nginx/mailcow_auth.conf
|
!data/conf/nginx/mailcow_auth.conf
|
||||||
|
data/conf/postfix/mta-sts-resolver
|
||||||
@@ -206,7 +206,7 @@ while true; do
|
|||||||
|
|
||||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
# Fetch certs for autoconfig and autodiscover subdomains
|
# Fetch certs for autoconfig and autodiscover subdomains
|
||||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
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
|
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
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ENV LC_ALL C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
||||||
&& ln -sf /bin/true /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 whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
|
||||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||||
COPY docker-entrypoint.sh /docker-entrypoint.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 \
|
RUN chmod +x /opt/postfix.sh \
|
||||||
/usr/local/bin/rspamd-pipe-ham \
|
/usr/local/bin/rspamd-pipe-ham \
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
trap "postfix stop" EXIT
|
trap "postfix stop" EXIT
|
||||||
|
|
||||||
[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
|
[[ ! -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
|
# Wait for MySQL to warm-up
|
||||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
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
|
EOF
|
||||||
fi
|
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
|
# Fix Postfix permissions
|
||||||
chown -R root:postfix /opt/postfix/conf/sql/ /opt/postfix/conf/custom_transport.pcre
|
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
|
chmod 640 /opt/postfix/conf/sql/*.cf /opt/postfix/conf/custom_transport.pcre
|
||||||
@@ -524,4 +546,4 @@ if [[ $? != 0 ]]; then
|
|||||||
else
|
else
|
||||||
postfix -c /opt/postfix/conf start
|
postfix -c /opt/postfix/conf start
|
||||||
sleep 126144000
|
sleep 126144000
|
||||||
fi
|
fi
|
||||||
@@ -11,6 +11,15 @@ stderr_logfile=/dev/stderr
|
|||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
autostart=true
|
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]
|
[program:postfix]
|
||||||
command=/opt/postfix.sh
|
command=/opt/postfix.sh
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
|
|||||||
@@ -48,13 +48,21 @@ http {
|
|||||||
listen {{ HTTP_PORT }} default_server;
|
listen {{ HTTP_PORT }} default_server;
|
||||||
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; }
|
if ( $request_uri ~* "%0A|%0D" ) { return 403; }
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
allow all;
|
allow all;
|
||||||
default_type "text/plain";
|
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 / {
|
location / {
|
||||||
return 301 https://$host$uri$is_args$args;
|
return 301 https://$host$uri$is_args$args;
|
||||||
}
|
}
|
||||||
@@ -82,7 +90,7 @@ http {
|
|||||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||||
ssl_certificate_key /etc/ssl/mail/key.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;
|
include /etc/nginx/includes/sites-default.conf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ location ^~ /.well-known/acme-challenge/ {
|
|||||||
allow all;
|
allow all;
|
||||||
default_type "text/plain";
|
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/caldav$ /SOGo/dav/ permanent;
|
||||||
rewrite ^/.well-known/carddav$ /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_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||||
smtp_sasl_security_options =
|
smtp_sasl_security_options =
|
||||||
smtp_sasl_mechanism_filter = plain, login
|
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
|
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||||
mail_name = Postcow
|
mail_name = Postcow
|
||||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
# 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);
|
$rl = ratelimit('get', 'domain', $domain);
|
||||||
$rlyhosts = relayhost('get');
|
$rlyhosts = relayhost('get');
|
||||||
$domain_footer = mailbox('get', 'domain_wide_footer', $domain);
|
$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 = 'edit/domain.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'acl' => $_SESSION['acl'],
|
'acl' => $_SESSION['acl'],
|
||||||
@@ -58,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
|||||||
'dkim' => dkim('details', $domain),
|
'dkim' => dkim('details', $domain),
|
||||||
'domain_details' => $result,
|
'domain_details' => $result,
|
||||||
'domain_footer' => $domain_footer,
|
'domain_footer' => $domain_footer,
|
||||||
|
'mta_sts' => $mta_sts,
|
||||||
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||||
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
||||||
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
|
'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
|
// Init records array
|
||||||
$spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />';
|
$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>';
|
$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();
|
$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(
|
$records[] = array(
|
||||||
$domain,
|
$domain,
|
||||||
'TXT',
|
'TXT',
|
||||||
@@ -341,15 +363,25 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($currents as &$current) {
|
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' &&
|
if ($current['type'] == 'TXT' &&
|
||||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||||
$record[2] == $dmarc_link) {
|
$record[2] == $dmarc_link) {
|
||||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
$current['txt'] = str_replace(' ', '', $current['txt']);
|
||||||
$state = $current[$data_field[$current['type']]] . state_optional;
|
$state = $current[$data_field[$current['type']]] . state_optional;
|
||||||
}
|
}
|
||||||
elseif ($current['type'] == 'TXT' &&
|
elseif ($current['type'] == 'TXT' &&
|
||||||
stripos($current['txt'], 'v=spf') === 0 &&
|
stripos($current['txt'], 'v=spf') === 0 &&
|
||||||
$record[2] == $spf_link) {
|
$record[2] == $spf_link) {
|
||||||
$state = state_nomatch;
|
$state = state_nomatch;
|
||||||
$rslt = get_spf_allowed_hosts($record[0], true);
|
$rslt = get_spf_allowed_hosts($record[0], true);
|
||||||
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
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;
|
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
|
||||||
}
|
}
|
||||||
elseif ($current['type'] == 'TXT' &&
|
elseif ($current['type'] == 'TXT' &&
|
||||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||||
stripos($record[2], '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', $current[$data_field[$current['type']]], $dkim_matches_current);
|
||||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
|
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
|
||||||
if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
|
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' &&
|
elseif ($current['type'] != 'TXT' &&
|
||||||
isset($data_field[$current['type']]) && $state != state_good) {
|
isset($data_field[$current['type']]) && $state != state_good) {
|
||||||
$state = state_nomatch;
|
$state = state_nomatch;
|
||||||
if ($current[$data_field[$current['type']]] == $record[2]) {
|
if ($current[$data_field[$current['type']]] == $record[2]) {
|
||||||
$state = state_good;
|
$state = state_good;
|
||||||
|
|||||||
@@ -1400,6 +1400,80 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
|
|
||||||
return mailbox('add', 'mailbox', $mailbox_attributes);
|
return mailbox('add', 'mailbox', $mailbox_attributes);
|
||||||
break;
|
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':
|
case 'resource':
|
||||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
$description = $_data['description'];
|
$description = $_data['description'];
|
||||||
@@ -3741,6 +3815,125 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
break;
|
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':
|
case 'resource':
|
||||||
if (!is_array($_data['name'])) {
|
if (!is_array($_data['name'])) {
|
||||||
$names = array();
|
$names = array();
|
||||||
@@ -5029,6 +5222,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'resource_details':
|
||||||
$resourcedata = array();
|
$resourcedata = array();
|
||||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
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(
|
$stmt->execute(array(
|
||||||
':domain' => $domain,
|
':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 `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`);");
|
$stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ function init_db_schema()
|
|||||||
try {
|
try {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
|
|
||||||
$db_version = "06082025_1611";
|
$db_version = "19082025_1436";
|
||||||
|
|
||||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$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"
|
"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(
|
"user_acl" => array(
|
||||||
"cols" => array(
|
"cols" => array(
|
||||||
"username" => "VARCHAR(255) NOT NULL",
|
"username" => "VARCHAR(255) NOT NULL",
|
||||||
|
|||||||
@@ -324,6 +324,9 @@ if (isset($_GET['query'])) {
|
|||||||
case "app-passwd":
|
case "app-passwd":
|
||||||
process_add_return(app_passwd('add', $attr));
|
process_add_return(app_passwd('add', $attr));
|
||||||
break;
|
break;
|
||||||
|
case "mta-sts":
|
||||||
|
process_add_return(mailbox('add', 'mta_sts', $attr));
|
||||||
|
break;
|
||||||
// return no route found if no case is matched
|
// return no route found if no case is matched
|
||||||
default:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
@@ -2001,6 +2004,9 @@ if (isset($_GET['query'])) {
|
|||||||
case "reset-password-notification":
|
case "reset-password-notification":
|
||||||
process_edit_return(reset_password('edit_notification', $attr));
|
process_edit_return(reset_password('edit_notification', $attr));
|
||||||
break;
|
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
|
// return no route found if no case is matched
|
||||||
default:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
|
|||||||
@@ -482,10 +482,13 @@
|
|||||||
"mailboxes_in_use": "Maximale Anzahl an Mailboxen muss größer oder gleich %d sein",
|
"mailboxes_in_use": "Maximale Anzahl an Mailboxen muss größer oder gleich %d sein",
|
||||||
"malformed_username": "Benutzername hat ein falsches Format",
|
"malformed_username": "Benutzername hat ein falsches Format",
|
||||||
"map_content_empty": "Inhalt darf nicht leer sein",
|
"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_alias_exceeded": "Anzahl an Alias-Adressen überschritten",
|
||||||
"max_mailbox_exceeded": "Anzahl an Mailboxen überschritten (%d von %d)",
|
"max_mailbox_exceeded": "Anzahl an Mailboxen überschritten (%d von %d)",
|
||||||
"max_quota_in_use": "Mailbox-Speicherplatzlimit muss größer oder gleich %d MiB sein",
|
"max_quota_in_use": "Mailbox-Speicherplatzlimit muss größer oder gleich %d MiB sein",
|
||||||
"maxquota_empty": "Max. Speicherplatz pro Mailbox darf nicht 0 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",
|
"mysql_error": "MySQL-Fehler: %s",
|
||||||
"network_host_invalid": "Netzwerk oder Host ungültig: %s",
|
"network_host_invalid": "Netzwerk oder Host ungültig: %s",
|
||||||
"next_hop_interferes": "%s verhindert das Hinzufügen von Next Hop %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",
|
"username_invalid": "Benutzername %s kann nicht verwendet werden",
|
||||||
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
|
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
|
||||||
"value_missing": "Bitte alle Felder ausfüllen",
|
"value_missing": "Bitte alle Felder ausfüllen",
|
||||||
|
"version_invalid": "Version %s ist ungültig",
|
||||||
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
|
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
|
||||||
"template_exists": "Vorlage %s existiert bereits",
|
"template_exists": "Vorlage %s existiert bereits",
|
||||||
"template_id_invalid": "Vorlagen-ID %s ungültig",
|
"template_id_invalid": "Vorlagen-ID %s ungültig",
|
||||||
@@ -703,6 +707,17 @@
|
|||||||
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
|
"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.",
|
"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)",
|
"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",
|
"multiple_bookings": "Mehrfaches Buchen",
|
||||||
"nexthop": "Next Hop",
|
"nexthop": "Next Hop",
|
||||||
"none_inherit": "Keine Auswahl / Erben",
|
"none_inherit": "Keine Auswahl / Erben",
|
||||||
@@ -852,7 +867,7 @@
|
|||||||
"add_tls_policy_map": "TLS-Richtlinieneintrag hinzufügen",
|
"add_tls_policy_map": "TLS-Richtlinieneintrag hinzufügen",
|
||||||
"address_rewriting": "Adressumschreibung",
|
"address_rewriting": "Adressumschreibung",
|
||||||
"alias": "Alias",
|
"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",
|
"alias_domain_backupmx": "Alias-Domain für Relay-Domain inaktiv",
|
||||||
"aliases": "Aliasse",
|
"aliases": "Aliasse",
|
||||||
"allow_from_smtp": "Nur folgende IPs für <b>SMTP</b> erlauben",
|
"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?",
|
"delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?",
|
||||||
"direct_aliases": "Direkte Alias-Adressen",
|
"direct_aliases": "Direkte Alias-Adressen",
|
||||||
"direct_aliases_desc": "Nur direkte Alias-Adressen werden für benutzerdefinierte Einstellungen berücksichtigt.",
|
"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": "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_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",
|
"eas_reset_now": "Jetzt zurücksetzen",
|
||||||
|
|||||||
@@ -483,10 +483,13 @@
|
|||||||
"mailboxes_in_use": "Max. mailboxes must be greater or equal to %d",
|
"mailboxes_in_use": "Max. mailboxes must be greater or equal to %d",
|
||||||
"malformed_username": "Malformed username",
|
"malformed_username": "Malformed username",
|
||||||
"map_content_empty": "Map content cannot be empty",
|
"map_content_empty": "Map content cannot be empty",
|
||||||
|
"max_age_invalid": "Max age %s is invalid",
|
||||||
"max_alias_exceeded": "Max. aliases exceeded",
|
"max_alias_exceeded": "Max. aliases exceeded",
|
||||||
"max_mailbox_exceeded": "Max. mailboxes exceeded (%d of %d)",
|
"max_mailbox_exceeded": "Max. mailboxes exceeded (%d of %d)",
|
||||||
"max_quota_in_use": "Mailbox quota must be greater or equal to %d MiB",
|
"max_quota_in_use": "Mailbox quota must be greater or equal to %d MiB",
|
||||||
"maxquota_empty": "Max. quota per mailbox must not be 0.",
|
"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",
|
"mysql_error": "MySQL error: %s",
|
||||||
"network_host_invalid": "Invalid network or host: %s",
|
"network_host_invalid": "Invalid network or host: %s",
|
||||||
"next_hop_interferes": "%s interferes with nexthop %s",
|
"next_hop_interferes": "%s interferes with nexthop %s",
|
||||||
@@ -550,6 +553,7 @@
|
|||||||
"username_invalid": "Username %s cannot be used",
|
"username_invalid": "Username %s cannot be used",
|
||||||
"validity_missing": "Please assign a period of validity",
|
"validity_missing": "Please assign a period of validity",
|
||||||
"value_missing": "Please provide all values",
|
"value_missing": "Please provide all values",
|
||||||
|
"version_invalid": "Version %s is invalid",
|
||||||
"yotp_verification_failed": "Yubico OTP verification failed: %s"
|
"yotp_verification_failed": "Yubico OTP verification failed: %s"
|
||||||
},
|
},
|
||||||
"datatables": {
|
"datatables": {
|
||||||
@@ -704,6 +708,17 @@
|
|||||||
"maxbytespersecond": "Max. bytes per second <br><small>(0 = unlimited)</small>",
|
"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.",
|
"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)",
|
"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",
|
"multiple_bookings": "Multiple bookings",
|
||||||
"none_inherit": "None / Inherit",
|
"none_inherit": "None / Inherit",
|
||||||
"nexthop": "Next hop",
|
"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="#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="#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="#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>
|
<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>
|
</ul>
|
||||||
<hr class="d-none d-md-block">
|
<hr class="d-none d-md-block">
|
||||||
@@ -278,6 +279,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 id="dfooter" class="tab-pane fade" role="tabpanel" aria-labelledby="domain-footer">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex d-md-none fs-5">
|
<div class="card-header d-flex d-md-none fs-5">
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ services:
|
|||||||
- dovecot
|
- dovecot
|
||||||
|
|
||||||
postfix-mailcow:
|
postfix-mailcow:
|
||||||
image: ghcr.io/mailcow/postfix:1.80
|
image: ghcr.io/mailcow/postfix:1.81
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql-mailcow:
|
mysql-mailcow:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
@@ -440,7 +440,7 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
unbound-mailcow:
|
unbound-mailcow:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
image: ghcr.io/mailcow/acme:1.93
|
image: ghcr.io/mailcow/acme:1.94
|
||||||
dns:
|
dns:
|
||||||
- ${IPV4_NETWORK:-172.22.1}.254
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
Reference in New Issue
Block a user