Compare commits

...

45 Commits

Author SHA1 Message Date
FreddleSpl0it
4845928e7a Merge pull request #7027 from mailcow/staging
[Hotfix] Update 2026-01
2026-01-29 10:31:23 +01:00
FreddleSpl0it
caaa4a414d [Web] Fix datatables search after PR #7022 2026-01-29 10:26:44 +01:00
FreddleSpl0it
4ccfedd6b3 Merge pull request #7024 from mailcow/staging
🐄🛡️ January 2026 Update | Limited EAS/DAV Access and Restricted Alias Sending
2026-01-29 07:25:09 +01:00
FreddleSpl0it
c3d841340c [Dovecot][PHP][SOGo] Update Images 2026-01-28 11:28:36 +01:00
FreddleSpl0it
b8cd00111f Merge pull request #7007 from moregeek/feat/allow_preset_passwords
feat: allow preset of passwords via environment vars
2026-01-28 10:18:56 +01:00
FreddleSpl0it
81cda80651 Merge pull request #7021 from mailcow/feat/restrict-alias-sending
[Postfix] Configurable send permissions for alias addresses
2026-01-28 10:03:02 +01:00
FreddleSpl0it
c1d4f04c22 Merge branch 'staging' into feat/restrict-alias-sending 2026-01-28 10:02:03 +01:00
FreddleSpl0it
82276cd1ca Merge pull request #7022 from mailcow/feat/eas-dav-access
[Web] Allow admins to limit EAS and DAV access for mailbox users
2026-01-28 09:54:47 +01:00
FreddleSpl0it
56ea4302ed [Web] Allow admins to limit EAS and DAV access for mailbox users 2026-01-28 09:49:33 +01:00
FreddleSpl0it
c06112b26e [Postfix] Configurable send permissions for alias addresses 2026-01-27 09:05:51 +01:00
FreddleSpl0it
aa5a4f0998 Merge pull request #6710 from mailcow/renovate/tianon-gosu-1.x
chore(deps): update dependency tianon/gosu to v1.19
2026-01-27 08:09:31 +01:00
FreddleSpl0it
bf4f471cfd Merge pull request #6837 from mailcow/renovate/php-memcached-dev-php-memcached-3.x
chore(deps): update dependency php-memcached-dev/php-memcached to v3.4.0
2026-01-27 08:08:50 +01:00
FreddleSpl0it
978bff9dbc Merge pull request #6867 from DiscoNova/feat/possible-to-disable-logins-from-autoprotocol-domains
[Web] Disable login UI on autoprotocol domains
2026-01-27 08:08:12 +01:00
FreddleSpl0it
869d9af7dd Merge pull request #6901 from mailcow/renovate/phpredis-phpredis-6.x
chore(deps): update dependency phpredis/phpredis to v6.3.0
2026-01-27 08:05:58 +01:00
FreddleSpl0it
af10499ecb Merge pull request #6927 from mailcow/renovate/imagick-imagick-3.x
chore(deps): update dependency imagick/imagick to v3.8.1
2026-01-27 08:04:51 +01:00
FreddleSpl0it
a1a4d8ff98 Merge pull request #6947 from mailcow/renovate/krakjoe-apcu-5.x
chore(deps): update dependency krakjoe/apcu to v5.1.28
2026-01-27 08:04:24 +01:00
FreddleSpl0it
95d61e8aa2 Merge pull request #6980 from bluewalk/feat/issue-6489
Configurable displayName(s) - Fixes issue #6489
2026-01-27 08:02:20 +01:00
FreddleSpl0it
ec8dd1a54f Merge pull request #6990 from psuet/mobileconfig-with-password-complexity
fix: Password for mobileconfig that conforms to password-complexity policy
2026-01-27 07:56:35 +01:00
milkmaker
382ee34d0e [Web] Updated lang.hu-hu.json (#7020)
Co-authored-by: Sándor <me-github@sandros.hu>
2026-01-26 20:15:47 +01:00
milkmaker
0999c9e9ab Translations update from Weblate (#7014)
* [Web] Updated lang.zh-cn.json

Co-authored-by: 雨 <luotianyi@luotianyi.me>

* [Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: 雨 <luotianyi@luotianyi.me>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2026-01-23 22:02:55 +01:00
Stefan Morgenthaler
c485968e7f feat: allow preset of passwords via environment vars
Signed-off-by: Stefan Morgenthaler <dev@morgenthaler.at>
2026-01-14 11:42:15 +01:00
milkmaker
e727620bd3 Translations update from Weblate (#7002)
* [Web] Updated lang.zh-cn.json

Co-authored-by: ガラスのような夢 <i@msdnicrosoft.work>

* [Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>

---------

Co-authored-by: ガラスのような夢 <i@msdnicrosoft.work>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2026-01-07 17:23:31 +01:00
milkmaker
71fa3ecebc update postscreen_access.cidr (#6987) 2026-01-07 17:22:01 +01:00
Paul Sütterlin
70101d1187 fix: Password for mobileconfig that conforms to password-complexity policy 2026-01-01 16:57:21 +01:00
bluewalk
c060c205d3 Fixes issue #6489 2025-12-21 16:56:16 +01:00
Copilot
038b2efb75 Add MTA-STS support for alias domains (#6972)
* Initial plan

* Add MTA-STS support for alias domains

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Improve domain normalization and code style in mta-sts.php

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Add error handling for idn_to_ascii in mta-sts.php

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Add database error handling for alias domain query

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Add ACME certificate support for MTA-STS on alias domains

Query alias_domain table to find aliases with MTA-STS enabled target domains and request certificates for mta-sts.<alias-domain> subdomains.

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* compose: bump image tag to 1.95

* Add MTA-STS DNS records display for alias domains in UI

When viewing an alias domain's DNS diagnostics, check if the target domain has MTA-STS enabled and display the required DNS records for the alias domain.

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-12-15 16:29:21 +01:00
DerLinkman
1fe4cd03e9 ui: fix global filters ui tickbox reappearing (#6966) 2025-12-12 16:01:18 +01:00
milkmaker
12e02e67ff Translations update from Weblate (#6965)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Keo <contact@kbl.netlib.re>

* [Web] Updated lang.pt-pt.json

Co-authored-by: Germano Pires Ferreira <germanopires@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>

---------

Co-authored-by: Keo <contact@kbl.netlib.re>
Co-authored-by: Germano Pires Ferreira <germanopires@gmail.com>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-12-12 15:21:04 +01:00
Ashitaka
e8d9315d4a Merge pull request #6905 from Ashitaka57/6646-pbkdf2-sha512-verify-hash
Support for PBKDF2-SHA512 hash algorithm in verify_hash() (FreeIPA compatibility) (issue 6646)
2025-12-12 14:08:21 +01:00
DerLinkman
d977ddb501 backup: add image prefetch function to verify latest image is used 2025-12-12 14:07:57 +01:00
DerLinkman
e76f5237ed ofelia: revert fixed cron syntax for sa-rules download 2025-12-12 14:07:47 +01:00
Copilot
c11ed5dd1e Prevent duplicate/plaintext login announcement rendering (#6963)
* Initial plan

* Fix duplicate login announcement display

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
2025-12-12 14:07:36 +01:00
DerLinkman
b6f57dfb78 rspamd: update to 3.14.2 2025-12-12 14:06:49 +01:00
Copilot
3ebf2c2d2d Prevent duplicate/plaintext login announcement rendering (#6963)
* Initial plan

* Fix duplicate login announcement display

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
2025-12-12 12:34:20 +01:00
DerLinkman
1bac6f1ee7 ofelia: revert fixed cron syntax for sa-rules download 2025-12-11 13:29:11 +01:00
DerLinkman
67e7acd6bd rspamd: upgrade to 3.14.1, trixie rebuild + bcc forwarded hosts fix (#6958)
* rspamd: fix bcc + subadress handling when using forward hosts

* rspamd: build against trixie + use version 3.14.1
2025-12-11 09:45:56 +01:00
renovate[bot]
910ce573d6 chore(deps): update peter-evans/create-pull-request action to v8 (#6953) 2025-12-10 19:48:02 +01:00
renovate[bot]
689336b3e1 chore(deps): update dependency tianon/gosu to v1.19
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:59 +00:00
renovate[bot]
01cf72cdef chore(deps): update dependency phpredis/phpredis to v6.3.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:54 +00:00
renovate[bot]
4cdb97c699 chore(deps): update dependency php-memcached-dev/php-memcached to v3.4.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:50 +00:00
renovate[bot]
1bd795a9c6 chore(deps): update dependency krakjoe/apcu to v5.1.28
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:42 +00:00
renovate[bot]
39f29e6c30 chore(deps): update dependency imagick/imagick to v3.8.1
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:38 +00:00
Ashitaka
1ab6af21e3 Merge pull request #6905 from Ashitaka57/6646-pbkdf2-sha512-verify-hash
Support for PBKDF2-SHA512 hash algorithm in verify_hash() (FreeIPA compatibility) (issue 6646)
2025-12-10 11:41:06 +01:00
DerLinkman
5d95c48e0d backup: add image prefetch function to verify latest image is used 2025-12-10 08:43:04 +01:00
Markku Post
95e0608749 [Web] Disable login on autodiscover/autoconfig domains
Autodiscover and autoconfig domains (autodiscover.*, autoconfig.*) are intended solely for client autoconfiguration endpoints and should not display the mailcow login page. This change check the hostname and disables unauthenticated users from seeing the login page on those domains; HTTP 404 response is returned when necessary.
2025-10-24 06:03:40 +03:00
45 changed files with 913 additions and 293 deletions

View File

@@ -22,7 +22,7 @@ jobs:
bash helper-scripts/update_postscreen_whitelist.sh bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }} token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr commit-message: update postscreen_access.cidr

View File

@@ -246,6 +246,25 @@ while true; do
done done
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}") VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
done done
# Fetch alias domains where target domain has MTA-STS enabled
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
if [[ $? -eq 0 ]]; then
while read alias_domain; do
if [[ -z "${alias_domain}" ]]; then
# ignore empty lines
continue
fi
# Only add mta-sts subdomain for alias domains
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "mta-sts.${alias_domain}"; then
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
fi
fi
done <<< "${SQL_ALIAS_DOMAINS}"
fi
fi
fi fi
if check_domain ${MAILCOW_HOSTNAME}; then if check_domain ${MAILCOW_HOSTNAME}; then

View File

@@ -3,7 +3,7 @@ FROM alpine:3.21
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.17 ARG GOSU_VERSION=1.19
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8

View File

@@ -3,15 +3,15 @@ FROM php:8.2-fpm-alpine3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.27 ARG APCU_PECL_VERSION=5.1.28
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.0 ARG IMAGICK_PECL_VERSION=3.8.1
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.9 ARG MAILPARSE_PECL_VERSION=3.1.9
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.3.0 ARG MEMCACHED_PECL_VERSION=3.4.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.2.0 ARG REDIS_PECL_VERSION=6.3.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.8.6 ARG COMPOSER_VERSION=2.8.6

View File

@@ -329,14 +329,17 @@ query = SELECT goto FROM alias
SELECT id FROM alias SELECT id FROM alias
WHERE address='%s' WHERE address='%s'
AND (active='1' OR active='2') AND (active='1' OR active='2')
AND sender_allowed='1'
), ( ), (
SELECT id FROM alias SELECT id FROM alias
WHERE address='@%d' WHERE address='@%d'
AND (active='1' OR active='2') AND (active='1' OR active='2')
AND sender_allowed='1'
) )
) )
) )
AND active='1' AND active='1'
AND sender_allowed='1'
AND (domain IN AND (domain IN
(SELECT domain FROM domain (SELECT domain FROM domain
WHERE domain='%d' WHERE domain='%d'

View File

@@ -1,9 +1,9 @@
FROM debian:bookworm-slim FROM debian:trixie-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
ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278 ARG RSPAMD_VER=rspamd_3.14.2-82~90302bc
ARG CODENAME=bookworm ARG CODENAME=trixie
ENV LC_ALL=C ENV LC_ALL=C
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \

View File

@@ -6,7 +6,7 @@ ARG DEBIAN_FRONTEND=noninteractive
ARG DEBIAN_VERSION=bookworm ARG DEBIAN_VERSION=bookworm
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/ ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.17 ARG GOSU_VERSION=1.19
ENV LC_ALL=C ENV LC_ALL=C
# Prerequisites # Prerequisites

View File

@@ -80,14 +80,21 @@ if ($isSOGoRequest) {
} }
if ($result === false){ if ($result === false){
// If it's a SOGo Request, don't check for protocol access // If it's a SOGo Request, don't check for protocol access
$service = ($isSOGoRequest) ? false : array($post['service'] => true); if ($isSOGoRequest) {
$result = apppass_login($post['username'], $post['password'], $service, array( $service = 'SOGO';
$post['service'] = 'NONE';
} else {
$service = $post['service'];
}
$result = apppass_login($post['username'], $post['password'], array(
'service' => $post['service'],
'is_internal' => true, 'is_internal' => true,
'remote_addr' => $post['real_rip'] 'remote_addr' => $post['real_rip']
)); ));
if ($result) { if ($result) {
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']); error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']); set_sasl_log($post['username'], $post['real_rip'], $service);
} }
} }
if ($result === false){ if ($result === false){

View File

@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Mon Dec 1 00:24:43 UTC 2025 # Whitelist generated by Postwhite v3.4 on Thu Jan 1 00:24:01 UTC 2026
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2186 total rules # 2105 total rules
2a00:1450:4000::/36 permit 2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit 2a01:111:f400::/48 permit
2a01:111:f403:2800::/53 permit 2a01:111:f403:2800::/53 permit
@@ -54,8 +54,8 @@
8.36.116.0/24 permit 8.36.116.0/24 permit
8.39.144.0/24 permit 8.39.144.0/24 permit
12.130.86.238 permit 12.130.86.238 permit
13.107.213.69 permit 13.107.213.38 permit
13.107.246.69 permit 13.107.246.38 permit
13.108.16.0/20 permit 13.108.16.0/20 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
13.110.209.0/24 permit 13.110.209.0/24 permit
@@ -65,7 +65,6 @@
13.111.191.0/24 permit 13.111.191.0/24 permit
13.216.7.111 permit 13.216.7.111 permit
13.216.54.180 permit 13.216.54.180 permit
13.247.164.219 permit
15.200.21.50 permit 15.200.21.50 permit
15.200.44.248 permit 15.200.44.248 permit
15.200.201.185 permit 15.200.201.185 permit
@@ -296,14 +295,6 @@
52.94.124.0/28 permit 52.94.124.0/28 permit
52.95.48.152/29 permit 52.95.48.152/29 permit
52.95.49.88/29 permit 52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.172.98 permit
52.96.222.194 permit
52.96.222.226 permit
52.96.223.2 permit
52.96.228.130 permit
52.96.229.242 permit
52.100.0.0/15 permit 52.100.0.0/15 permit
52.102.0.0/16 permit 52.102.0.0/16 permit
52.103.0.0/17 permit 52.103.0.0/17 permit
@@ -397,19 +388,8 @@
64.207.219.143 permit 64.207.219.143 permit
64.233.160.0/19 permit 64.233.160.0/19 permit
65.52.80.137 permit 65.52.80.137 permit
65.54.121.120/29 permit
65.55.29.77 permit 65.55.29.77 permit
65.55.33.64/28 permit
65.55.42.224/28 permit 65.55.42.224/28 permit
65.55.52.224/27 permit
65.55.78.128/25 permit
65.55.81.48/28 permit
65.55.94.0/25 permit
65.55.113.64/26 permit
65.55.126.0/25 permit
65.55.174.0/25 permit
65.55.178.128/27 permit
65.55.234.192/26 permit
65.110.161.77 permit 65.110.161.77 permit
65.123.29.213 permit 65.123.29.213 permit
65.123.29.220 permit 65.123.29.220 permit
@@ -529,7 +509,6 @@
69.169.224.0/20 permit 69.169.224.0/20 permit
69.171.232.0/24 permit 69.171.232.0/24 permit
69.171.244.0/23 permit 69.171.244.0/23 permit
70.37.151.128/25 permit
70.42.149.35 permit 70.42.149.35 permit
72.3.185.0/24 permit 72.3.185.0/24 permit
72.14.192.0/18 permit 72.14.192.0/18 permit
@@ -654,12 +633,18 @@
81.169.146.245 permit 81.169.146.245 permit
81.169.146.246 permit 81.169.146.246 permit
81.223.46.0/27 permit 81.223.46.0/27 permit
82.165.159.2 permit
82.165.159.3 permit
82.165.159.4 permit
82.165.159.12 permit 82.165.159.12 permit
82.165.159.13 permit 82.165.159.13 permit
82.165.159.14 permit 82.165.159.14 permit
82.165.159.34 permit
82.165.159.35 permit
82.165.159.40 permit 82.165.159.40 permit
82.165.159.41 permit 82.165.159.41 permit
82.165.159.42 permit 82.165.159.42 permit
82.165.159.45 permit
82.165.159.130 permit 82.165.159.130 permit
82.165.159.131 permit 82.165.159.131 permit
85.9.206.169 permit 85.9.206.169 permit
@@ -715,8 +700,6 @@
91.198.2.0/24 permit 91.198.2.0/24 permit
91.211.240.0/22 permit 91.211.240.0/22 permit
94.236.119.0/26 permit 94.236.119.0/26 permit
94.245.112.0/27 permit
94.245.112.10/31 permit
95.131.104.0/21 permit 95.131.104.0/21 permit
95.217.114.154 permit 95.217.114.154 permit
96.43.144.0/20 permit 96.43.144.0/20 permit
@@ -1354,11 +1337,6 @@
108.179.144.0/20 permit 108.179.144.0/20 permit
109.224.244.0/24 permit 109.224.244.0/24 permit
109.237.142.0/24 permit 109.237.142.0/24 permit
111.221.23.128/25 permit
111.221.26.0/27 permit
111.221.66.0/25 permit
111.221.69.128/25 permit
111.221.112.0/21 permit
112.19.199.64/29 permit 112.19.199.64/29 permit
112.19.242.64/29 permit 112.19.242.64/29 permit
116.214.12.47 permit 116.214.12.47 permit
@@ -1420,6 +1398,7 @@
129.153.194.228 permit 129.153.194.228 permit
129.154.255.129 permit 129.154.255.129 permit
129.158.56.255 permit 129.158.56.255 permit
129.158.62.153 permit
129.159.22.159 permit 129.159.22.159 permit
129.159.87.137 permit 129.159.87.137 permit
129.213.195.191 permit 129.213.195.191 permit
@@ -1441,16 +1420,6 @@
134.170.143.0/24 permit 134.170.143.0/24 permit
134.170.174.0/24 permit 134.170.174.0/24 permit
135.84.216.0/22 permit 135.84.216.0/22 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.162.0/24 permit
136.143.176.0/24 permit
136.143.177.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
136.143.190.0/23 permit
136.146.128.0/20 permit 136.146.128.0/20 permit
136.147.128.0/20 permit 136.147.128.0/20 permit
136.147.135.0/24 permit 136.147.135.0/24 permit
@@ -1468,6 +1437,8 @@
139.138.58.119 permit 139.138.58.119 permit
139.180.17.0/24 permit 139.180.17.0/24 permit
140.238.148.191 permit 140.238.148.191 permit
141.148.55.217 permit
141.148.91.244 permit
141.148.159.229 permit 141.148.159.229 permit
141.193.32.0/23 permit 141.193.32.0/23 permit
141.193.184.32/27 permit 141.193.184.32/27 permit
@@ -1513,6 +1484,7 @@
149.72.234.184 permit 149.72.234.184 permit
149.72.248.236 permit 149.72.248.236 permit
149.97.173.180 permit 149.97.173.180 permit
150.136.21.199 permit
150.230.98.160 permit 150.230.98.160 permit
151.145.38.14 permit 151.145.38.14 permit
152.67.105.195 permit 152.67.105.195 permit
@@ -1522,17 +1494,7 @@
155.248.220.138 permit 155.248.220.138 permit
155.248.234.149 permit 155.248.234.149 permit
155.248.237.141 permit 155.248.237.141 permit
157.55.9.128/25 permit
157.55.11.0/25 permit
157.55.49.0/25 permit
157.55.61.0/24 permit
157.55.157.128/25 permit
157.55.225.0/25 permit
157.56.24.0/25 permit
157.56.120.128/26 permit 157.56.120.128/26 permit
157.56.232.0/21 permit
157.56.240.0/20 permit
157.56.248.0/21 permit
157.58.30.128/25 permit 157.58.30.128/25 permit
157.58.196.96/29 permit 157.58.196.96/29 permit
157.58.249.3 permit 157.58.249.3 permit
@@ -1582,6 +1544,9 @@
163.114.135.16 permit 163.114.135.16 permit
163.116.128.0/17 permit 163.116.128.0/17 permit
163.192.116.87 permit 163.192.116.87 permit
163.192.125.176 permit
163.192.196.146 permit
163.192.204.161 permit
164.152.23.32 permit 164.152.23.32 permit
164.152.25.241 permit 164.152.25.241 permit
164.177.132.168/30 permit 164.177.132.168/30 permit
@@ -1614,6 +1579,7 @@
168.245.12.252 permit 168.245.12.252 permit
168.245.46.9 permit 168.245.46.9 permit
168.245.127.231 permit 168.245.127.231 permit
170.9.232.254 permit
170.10.128.0/24 permit 170.10.128.0/24 permit
170.10.129.0/24 permit 170.10.129.0/24 permit
170.10.132.56/29 permit 170.10.132.56/29 permit
@@ -1623,7 +1589,6 @@
173.0.84.224/27 permit 173.0.84.224/27 permit
173.0.94.244/30 permit 173.0.94.244/30 permit
173.194.0.0/16 permit 173.194.0.0/16 permit
173.194.0.0/17 permit
173.203.79.182 permit 173.203.79.182 permit
173.203.81.39 permit 173.203.81.39 permit
173.224.161.128/25 permit 173.224.161.128/25 permit
@@ -1852,7 +1817,6 @@
204.14.232.64/28 permit 204.14.232.64/28 permit
204.14.234.64/28 permit 204.14.234.64/28 permit
204.75.142.0/24 permit 204.75.142.0/24 permit
204.79.197.212 permit
204.92.114.187 permit 204.92.114.187 permit
204.92.114.203 permit 204.92.114.203 permit
204.92.114.204/31 permit 204.92.114.204/31 permit
@@ -1878,23 +1842,13 @@
206.165.246.80/29 permit 206.165.246.80/29 permit
206.191.224.0/19 permit 206.191.224.0/19 permit
206.246.157.1 permit 206.246.157.1 permit
207.46.4.128/25 permit
207.46.22.35 permit 207.46.22.35 permit
207.46.50.72 permit 207.46.50.72 permit
207.46.50.82 permit 207.46.50.82 permit
207.46.50.192/26 permit
207.46.50.224 permit
207.46.52.71 permit 207.46.52.71 permit
207.46.52.79 permit 207.46.52.79 permit
207.46.58.128/25 permit
207.46.116.128/29 permit
207.46.132.128/27 permit
207.46.198.0/25 permit
207.46.200.0/27 permit
207.67.38.0/24 permit 207.67.38.0/24 permit
207.67.98.192/27 permit 207.67.98.192/27 permit
207.68.176.0/26 permit
207.68.176.96/27 permit
207.97.204.96/29 permit 207.97.204.96/29 permit
207.126.144.0/20 permit 207.126.144.0/20 permit
207.171.160.0/19 permit 207.171.160.0/19 permit
@@ -1993,11 +1947,19 @@
212.82.111.228/31 permit 212.82.111.228/31 permit
212.82.111.230 permit 212.82.111.230 permit
212.123.28.40 permit 212.123.28.40 permit
212.227.15.3 permit
212.227.15.4 permit
212.227.15.5 permit
212.227.15.6 permit
212.227.15.7 permit 212.227.15.7 permit
212.227.15.8 permit 212.227.15.8 permit
212.227.15.14 permit
212.227.15.15 permit 212.227.15.15 permit
212.227.15.18 permit 212.227.15.18 permit
212.227.15.19 permit 212.227.15.19 permit
212.227.15.25 permit
212.227.15.26 permit
212.227.15.29 permit
212.227.15.44 permit 212.227.15.44 permit
212.227.15.45 permit 212.227.15.45 permit
212.227.15.46 permit 212.227.15.46 permit
@@ -2005,11 +1967,17 @@
212.227.15.50 permit 212.227.15.50 permit
212.227.15.52 permit 212.227.15.52 permit
212.227.15.53 permit 212.227.15.53 permit
212.227.15.54 permit
212.227.15.55 permit
212.227.17.1 permit 212.227.17.1 permit
212.227.17.2 permit 212.227.17.2 permit
212.227.17.7 permit 212.227.17.7 permit
212.227.17.11 permit
212.227.17.12 permit
212.227.17.16 permit 212.227.17.16 permit
212.227.17.17 permit 212.227.17.17 permit
212.227.17.18 permit
212.227.17.19 permit
212.227.17.20 permit 212.227.17.20 permit
212.227.17.21 permit 212.227.17.21 permit
212.227.17.22 permit 212.227.17.22 permit
@@ -2035,8 +2003,6 @@
213.199.128.145 permit 213.199.128.145 permit
213.199.138.181 permit 213.199.138.181 permit
213.199.138.191 permit 213.199.138.191 permit
213.199.161.128/27 permit
213.199.177.0/26 permit
216.17.150.242 permit 216.17.150.242 permit
216.17.150.251 permit 216.17.150.251 permit
216.24.224.0/20 permit 216.24.224.0/20 permit
@@ -2064,7 +2030,6 @@
216.39.62.60/31 permit 216.39.62.60/31 permit
216.39.62.136/29 permit 216.39.62.136/29 permit
216.39.62.144/31 permit 216.39.62.144/31 permit
216.58.192.0/19 permit
216.66.217.240/29 permit 216.66.217.240/29 permit
216.71.138.33 permit 216.71.138.33 permit
216.71.152.207 permit 216.71.152.207 permit
@@ -2094,6 +2059,8 @@
216.205.24.0/24 permit 216.205.24.0/24 permit
216.221.160.0/19 permit 216.221.160.0/19 permit
216.239.32.0/19 permit 216.239.32.0/19 permit
217.72.192.77 permit
217.72.192.78 permit
217.77.141.52 permit 217.77.141.52 permit
217.77.141.59 permit 217.77.141.59 permit
217.175.194.0/24 permit 217.175.194.0/24 permit

View File

@@ -146,8 +146,171 @@ rspamd_config:register_symbol({
return false return false
end end
-- Helper function to parse IPv6 into 8 segments
local function ipv6_to_segments(ip_str)
-- Remove zone identifier if present (e.g., %eth0)
ip_str = ip_str:gsub("%%.*$", "")
local segments = {}
-- Handle :: compression
if ip_str:find('::') then
local before, after = ip_str:match('^(.*)::(.*)$')
before = before or ''
after = after or ''
local before_parts = {}
local after_parts = {}
if before ~= '' then
for seg in before:gmatch('[^:]+') do
table.insert(before_parts, tonumber(seg, 16) or 0)
end
end
if after ~= '' then
for seg in after:gmatch('[^:]+') do
table.insert(after_parts, tonumber(seg, 16) or 0)
end
end
-- Add before segments
for _, seg in ipairs(before_parts) do
table.insert(segments, seg)
end
-- Add compressed zeros
local zeros_needed = 8 - #before_parts - #after_parts
for i = 1, zeros_needed do
table.insert(segments, 0)
end
-- Add after segments
for _, seg in ipairs(after_parts) do
table.insert(segments, seg)
end
else
-- No compression
for seg in ip_str:gmatch('[^:]+') do
table.insert(segments, tonumber(seg, 16) or 0)
end
end
-- Ensure we have exactly 8 segments
while #segments < 8 do
table.insert(segments, 0)
end
return segments
end
-- Generate all common IPv6 notations
local function get_ipv6_variants(ip_str)
local variants = {}
local seen = {}
local function add_variant(v)
if v and not seen[v] then
table.insert(variants, v)
seen[v] = true
end
end
-- For IPv4, just return the original
if not ip_str:find(':') then
add_variant(ip_str)
return variants
end
local segments = ipv6_to_segments(ip_str)
-- 1. Fully expanded form (all zeros shown as 0000)
local expanded_parts = {}
for _, seg in ipairs(segments) do
table.insert(expanded_parts, string.format('%04x', seg))
end
add_variant(table.concat(expanded_parts, ':'))
-- 2. Standard form (no leading zeros, but all segments present)
local standard_parts = {}
for _, seg in ipairs(segments) do
table.insert(standard_parts, string.format('%x', seg))
end
add_variant(table.concat(standard_parts, ':'))
-- 3. Find all possible :: compressions
-- RFC 5952: compress the longest run of consecutive zeros
-- But we need to check all possibilities since Redis might have any form
-- Find all zero runs
local zero_runs = {}
local in_run = false
local run_start = 0
local run_length = 0
for i = 1, 8 do
if segments[i] == 0 then
if not in_run then
in_run = true
run_start = i
run_length = 1
else
run_length = run_length + 1
end
else
if in_run then
if run_length >= 1 then -- Allow single zero compression too
table.insert(zero_runs, {start = run_start, length = run_length})
end
in_run = false
end
end
end
-- Don't forget the last run
if in_run and run_length >= 1 then
table.insert(zero_runs, {start = run_start, length = run_length})
end
-- Generate variant for each zero run compression
for _, run in ipairs(zero_runs) do
local parts = {}
-- Before compression
for i = 1, run.start - 1 do
table.insert(parts, string.format('%x', segments[i]))
end
-- The compression
if run.start == 1 then
table.insert(parts, '')
table.insert(parts, '')
elseif run.start + run.length - 1 == 8 then
table.insert(parts, '')
table.insert(parts, '')
else
table.insert(parts, '')
end
-- After compression
for i = run.start + run.length, 8 do
table.insert(parts, string.format('%x', segments[i]))
end
local compressed = table.concat(parts, ':'):gsub('::+', '::')
add_variant(compressed)
end
return variants
end
local from_ip_string = tostring(ip) local from_ip_string = tostring(ip)
ip_check_table = {from_ip_string} local ip_check_table = {}
-- Add all variants of the exact IP
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
table.insert(ip_check_table, variant)
end
local maxbits = 128 local maxbits = 128
local minbits = 32 local minbits = 32
@@ -155,10 +318,18 @@ rspamd_config:register_symbol({
maxbits = 32 maxbits = 32
minbits = 8 minbits = 8
end end
-- Add all CIDR notations with variants
for i=maxbits,minbits,-1 do for i=maxbits,minbits,-1 do
local nip = ip:apply_mask(i):to_string() .. "/" .. i local masked_ip = ip:apply_mask(i)
table.insert(ip_check_table, nip) local cidr_base = masked_ip:to_string()
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
local cidr = variant .. "/" .. i
table.insert(ip_check_table, cidr)
end
end end
local function keep_spam_cb(err, data) local function keep_spam_cb(err, data)
if err then if err then
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err) rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
@@ -166,12 +337,15 @@ rspamd_config:register_symbol({
else else
for k,v in pairs(data) do for k,v in pairs(data) do
if (v and v ~= userdata and v == '1') then if (v and v ~= userdata and v == '1') then
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result") rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam') task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
task:set_flag('no_stat')
return
end end
end end
end end
end end
table.insert(ip_check_table, 1, 'KEEP_SPAM') table.insert(ip_check_table, 1, 'KEEP_SPAM')
local redis_ret_user = rspamd_redis_make_request(task, local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params redis_params, -- connect params
@@ -210,6 +384,7 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({ rspamd_config:register_symbol({
name = 'TAG_MOO', name = 'TAG_MOO',
type = 'postfilter', type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task) callback = function(task)
local util = require("rspamd_util") local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger" local rspamd_logger = require "rspamd_logger"
@@ -218,9 +393,6 @@ rspamd_config:register_symbol({
local rcpts = task:get_recipients('smtp') local rcpts = task:get_recipients('smtp')
local lua_util = require "lua_util" local lua_util = require "lua_util"
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
local function remove_moo_tag() local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false) local moo_tag_header = task:get_header('X-Moo-Tag', false)
if moo_tag_header then if moo_tag_header then
@@ -231,101 +403,149 @@ rspamd_config:register_symbol({
return true return true
end end
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then -- Check if we have exactly one recipient
local tag = tagged_rcpt[1].options[1] if not (rcpts and #rcpts == 1) then
rspamd_logger.infox("found tag: %s", tag) rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
local action = task:get_metric_action('default') remove_moo_tag()
rspamd_logger.infox("metric action now: %s", action) return
end
if action ~= 'no action' and action ~= 'greylist' then local rcpt_addr = rcpts[1]['addr']
rspamd_logger.infox("skipping tag handler for action: %s", action) local rcpt_user = rcpts[1]['user']
remove_moo_tag() local rcpt_domain = rcpts[1]['domain']
return true
-- Check if recipient has a tag (contains '+')
local tag = nil
if rcpt_user:find('%+') then
local base_user, tag_part = rcpt_user:match('^(.-)%+(.+)$')
if base_user and tag_part then
tag = tag_part
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
end end
end
local function http_callback(err_message, code, body, headers) if not tag then
if body ~= nil and body ~= "" then rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body) remove_moo_tag()
return
end
local function tag_callback_subject(err, data) -- Optional: Check if domain is a mailcow domain
if err or type(data) ~= 'string' then -- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err) -- If the mail is being delivered, we can assume it's valid
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
if not mailcow_domain then
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
end
local function tag_callback_subfolder(err, data) local action = task:get_metric_action('default')
if err or type(data) ~= 'string' then rspamd_logger.infox("TAG_MOO: metric action: %s", action)
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag()
else
rspamd_logger.infox("Add X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local redis_ret_subfolder = rspamd_redis_make_request(task, -- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
redis_params, -- connect params local allow_processing = false
body, -- hash key
false, -- is write if task.has_pre_result then
tag_callback_subfolder, --callback local has_pre, pre_action = task:has_pre_result()
'HGET', -- command if has_pre then
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
) if pre_action == 'accept' then
if not redis_ret_subfolder then allow_processing = true
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
end
end
end
-- Allow processing for mild actions or when we have pre-result accept
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
remove_moo_tag()
return true
end
rspamd_logger.infox("TAG_MOO: processing allowed")
local function http_callback(err_message, code, body, headers)
if body ~= nil and body ~= "" then
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
local function tag_callback_subject(err, data)
if err or type(data) ~= 'string' or data == '' then
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
local function tag_callback_subfolder(err, data)
if err or type(data) ~= 'string' or data == '' then
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag() remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end end
else
rspamd_logger.infox("user wants subject modified for tagged mail")
local sbj = task:get_header('Subject')
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end end
end
local redis_ret_subject = rspamd_redis_make_request(task, local redis_ret_subfolder = rspamd_redis_make_request(task,
redis_params, -- connect params redis_params, -- connect params
body, -- hash key body, -- hash key
false, -- is write false, -- is write
tag_callback_subject, --callback tag_callback_subfolder, --callback
'HGET', -- command 'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments {'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
) )
if not redis_ret_subject then if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
end
end
if rcpts and #rcpts == 1 then
for _,rcpt in ipairs(rcpts) do
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
remove_moo_tag() remove_moo_tag()
else
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt['addr']},
})
end end
else
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
local sbj = task:get_header('Subject') or ''
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end end
end end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
else
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
remove_moo_tag()
end
end
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
if #rcpt_split == 2 then
if rcpt_split[1]:match('^postmaster') then
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt_addr},
})
end end
else else
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
remove_moo_tag() remove_moo_tag()
end end
end, end,
@@ -335,6 +555,7 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({ rspamd_config:register_symbol({
name = 'BCC', name = 'BCC',
type = 'postfilter', type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task) callback = function(task)
local util = require("rspamd_util") local util = require("rspamd_util")
local rspamd_http = require "rspamd_http" local rspamd_http = require "rspamd_http"
@@ -363,11 +584,13 @@ rspamd_config:register_symbol({
local email_content = tostring(task:get_content()) local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..") email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail -- send mail
local from_smtp = task:get_from('smtp')
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
lua_smtp.sendmail({ lua_smtp.sendmail({
task = task, task = task,
host = os.getenv("IPV4_NETWORK") .. '.253', host = os.getenv("IPV4_NETWORK") .. '.253',
port = 591, port = 591,
from = task:get_from(stp)[1].addr, from = from_addr,
recipients = bcc_dest, recipients = bcc_dest,
helo = 'bcc', helo = 'bcc',
timeout = 20, timeout = 20,
@@ -397,27 +620,41 @@ rspamd_config:register_symbol({
end end
local action = task:get_metric_action('default') local action = task:get_metric_action('default')
rspamd_logger.infox("metric action now: %s", action) rspamd_logger.infox("BCC: metric action: %s", action)
-- Check for pre-result accept (e.g., from KEEP_SPAM)
local allow_bcc = false
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre and pre_action == 'accept' then
allow_bcc = true
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
end
end
-- Allow BCC for mild actions or when we have pre-result accept
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
rspamd_logger.infox("BCC: skipping for action: %s", action)
return
end
local function rcpt_callback(err_message, code, body, headers) local function rcpt_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then if err_message == nil and code == 201 and body ~= nil then
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
send_mail(task, body) send_mail(task, body)
end
end end
end end
local function from_callback(err_message, code, body, headers) local function from_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then if err_message == nil and code == 201 and body ~= nil then
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
send_mail(task, body) send_mail(task, body)
end
end end
end end
if rcpt_table then if rcpt_table then
for _,e in ipairs(rcpt_table) do for _,e in ipairs(rcpt_table) do
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e) rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
url='http://nginx:8081/bcc.php', url='http://nginx:8081/bcc.php',
@@ -430,7 +667,7 @@ rspamd_config:register_symbol({
if from_table then if from_table then
for _,e in ipairs(from_table) do for _,e in ipairs(from_table) do
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e) rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
url='http://nginx:8081/bcc.php', url='http://nginx:8081/bcc.php',
@@ -441,7 +678,7 @@ rspamd_config:register_symbol({
end end
end end
return true -- Don't return true to avoid symbol being logged
end, end,
priority = 20 priority = 20
}) })
@@ -708,4 +945,4 @@ rspamd_config:register_symbol({
return true return true
end, end,
priority = 1 priority = 1
}) })

View File

@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
<clientConfig version="1.1"> <clientConfig version="1.1">
<emailProvider id="<?=$mailcow_hostname; ?>"> <emailProvider id="<?=$mailcow_hostname; ?>">
<domain>%EMAILDOMAIN%</domain> <domain>%EMAILDOMAIN%</domain>
<displayName>A mailcow mail server</displayName> <displayName><?=$autodiscover_config['displayName']; ?></displayName>
<displayShortName>mail server</displayShortName> <displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
<incomingServer type="imap"> <incomingServer type="imap">
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname> <hostname><?=$autodiscover_config['imap']['server']; ?></hostname>

View File

@@ -79,7 +79,7 @@ if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
exit(0); exit(0);
} }
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE)); $login_role = check_login($login_user, $login_pass, array('service' => 'EAS'));
if ($login_role === "user") { if ($login_role === "user") {
header("Content-Type: application/xml"); header("Content-Type: application/xml");

View File

@@ -129,7 +129,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
); );
} }
$mta_sts = mailbox('get', 'mta_sts', $domain); // Check if domain is an alias domain and get target domain's MTA-STS
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
$mta_sts_domain = $domain;
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
// This is an alias domain, check target domain for MTA-STS
$mta_sts_domain = $alias_domain_details['target_domain'];
}
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) { if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) { if (!in_array($domain, $alias_domains)) {
$records[] = array( $records[] = array(

View File

@@ -1,10 +1,11 @@
<?php <?php
function check_login($user, $pass, $app_passwd_data = false, $extra = null) { function check_login($user, $pass, $extra = null) {
global $pdo; global $pdo;
global $redis; global $redis;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$role = $extra['role']; $role = $extra['role'];
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
// Try validate admin // Try validate admin
if (!isset($role) || $role == "admin") { if (!isset($role) || $role == "admin") {
@@ -25,34 +26,20 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
// Try validate app password // Try validate app password
if (!isset($role) || $role == "app") { if (!isset($role) || $role == "app") {
$result = apppass_login($user, $pass, $app_passwd_data); $result = apppass_login($user, $pass, $extra);
if ($result !== false) { if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'NONE';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $service, $pass); set_sasl_log($user, $real_rip, $extra['service'], $pass);
return $result; return $result;
} }
} }
// Try validate user // Try validate user
if (!isset($role) || $role == "user") { if (!isset($role) || $role == "user") {
$result = user_login($user, $pass); $result = user_login($user, $pass, $extra);
if ($result !== false) { if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'MAILCOWUI';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $service); set_sasl_log($user, $real_rip, $extra['service']);
return $result; return $result;
} }
} }
@@ -193,7 +180,7 @@ function user_login($user, $pass, $extra = null){
global $iam_settings; global $iam_settings;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$service = $extra['service']; $extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
if (!$is_internal){ if (!$is_internal){
@@ -236,10 +223,10 @@ function user_login($user, $pass, $extra = null){
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) { if (!empty($row)) {
// check if user has access to service (imap, smtp, pop3, sieve) if service is set // check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set
$row['attributes'] = json_decode($row['attributes'], true); $row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) { if ($extra['service'] != 'NONE') {
$key = strtolower($service) . "_access"; $key = strtolower($extra['service']) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false; return false;
} }
@@ -253,8 +240,8 @@ function user_login($user, $pass, $extra = null){
// check if user has access to service (imap, smtp, pop3, sieve) if service is set // check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true); $row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) { if ($extra['service'] != 'NONE') {
$key = strtolower($service) . "_access"; $key = strtolower($extra['service']) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false; return false;
} }
@@ -408,7 +395,7 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
function apppass_login($user, $pass, $app_passwd_data, $extra = null){ function apppass_login($user, $pass, $extra = null){
global $pdo; global $pdo;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
@@ -424,20 +411,8 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
return false; return false;
} }
$protocol = false; $extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
if ($app_passwd_data['eas']){ if (!$is_internal && $extra['service'] == 'NONE') {
$protocol = 'eas';
} else if ($app_passwd_data['dav']){
$protocol = 'dav';
} else if ($app_passwd_data['smtp']){
$protocol = 'smtp';
} else if ($app_passwd_data['imap']){
$protocol = 'imap';
} else if ($app_passwd_data['sieve']){
$protocol = 'sieve';
} else if ($app_passwd_data['pop3']){
$protocol = 'pop3';
} else if (!$is_internal) {
return false; return false;
} }
@@ -458,7 +433,7 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) { foreach ($rows as $row) {
if ($protocol && $row[$protocol . '_access'] != '1'){ if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
continue; continue;
} }

View File

@@ -205,6 +205,42 @@ function password_complexity($_action, $_data = null) {
break; break;
} }
} }
function password_generate(){
$password_complexity = password_complexity('get');
$min_length = max(16, intval($password_complexity['length']));
$lowercase = range('a', 'z');
$uppercase = range('A', 'Z');
$digits = range(0, 9);
$special_chars = str_split('!@#$%^&*()?=');
$password = [
$lowercase[random_int(0, count($lowercase) - 1)],
$uppercase[random_int(0, count($uppercase) - 1)],
$digits[random_int(0, count($digits) - 1)],
$special_chars[random_int(0, count($special_chars) - 1)],
];
$all = array_merge($lowercase, $uppercase, $digits, $special_chars);
while (count($password) < $min_length) {
$password[] = $all[random_int(0, count($all) - 1)];
}
// Cryptographically secure shuffle using Fisher-Yates algorithm
$count = count($password);
for ($i = $count - 1; $i > 0; $i--) {
$j = random_int(0, $i);
$temp = $password[$i];
$password[$i] = $password[$j];
$password[$j] = $temp;
}
return implode('', $password);
}
function password_check($password1, $password2) { function password_check($password1, $password2) {
$password_complexity = password_complexity('get'); $password_complexity = password_complexity('get');
@@ -814,6 +850,32 @@ function verify_hash($hash, $password) {
$hash = $components[4]; $hash = $components[4];
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash); return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
case "PBKDF2-SHA512":
// Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$<base64_salt>$<base64_hash>
$components = explode('$', $hash);
if (count($components) !== 3) return false;
// 1st part: iteration count (integer)
$iterations = intval($components[0]);
if ($iterations <= 0) return false;
// 2nd part: salt (base64-encoded)
$salt = $components[1];
// 3rd part: hash (base64-encoded)
$stored_hash_b64 = $components[2];
// Decode salt and hash from base64
$salt_bin = base64_decode($salt, true);
$hash_bin = base64_decode($stored_hash_b64, true);
if ($salt_bin === false || $hash_bin === false) return false;
// Get length of hash in bytes
$hash_len = strlen($hash_bin);
if ($hash_len === 0) return false;
// Calculate PBKDF2-SHA512 hash for provided password
$test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true);
return hash_equals($hash_bin, $test_hash);
case "PLAIN-MD4": case "PLAIN-MD4":
return hash_equals(hash('md4', $password), $hash); return hash_equals(hash('md4', $password), $hash);

View File

@@ -695,6 +695,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto'])); $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$internal = intval($_data['internal']); $internal = intval($_data['internal']);
$active = intval($_data['active']); $active = intval($_data['active']);
$sender_allowed = intval($_data['sender_allowed']);
$sogo_visible = intval($_data['sogo_visible']); $sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']); $goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']); $goto_spam = intval($_data['goto_spam']);
@@ -850,8 +851,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`) $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)"); VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :active)");
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) { if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
$stmt->execute(array( $stmt->execute(array(
':address' => '@'.$domain, ':address' => '@'.$domain,
@@ -862,6 +863,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active ':active' => $active
)); ));
} }
@@ -874,6 +876,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active ':active' => $active
)); ));
} }
@@ -1075,6 +1078,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']); $active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
@@ -1085,6 +1090,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$eas_access = (isset($_data['eas_access'])) ? intval($_data['eas_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$dav_access = (isset($_data['dav_access'])) ? intval($_data['dav_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0; $relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']); $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']); $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
@@ -1103,6 +1110,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'pop3_access' => strval($pop3_access), 'pop3_access' => strval($pop3_access),
'smtp_access' => strval($smtp_access), 'smtp_access' => strval($smtp_access),
'sieve_access' => strval($sieve_access), 'sieve_access' => strval($sieve_access),
'eas_access' => strval($eas_access),
'dav_access' => strval($dav_access),
'relayhost' => strval($relayhost), 'relayhost' => strval($relayhost),
'passwd_update' => time(), 'passwd_update' => time(),
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']), 'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
@@ -1721,12 +1730,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$attr['eas_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$attr['dav_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
} }
if (isset($_data['acl'])) { if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
@@ -2501,6 +2514,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (!empty($is_now)) { if (!empty($is_now)) {
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal']; $internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible']; $sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0; $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0; $goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
@@ -2686,6 +2700,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`goto` = :goto, `goto` = :goto,
`sogo_visible`= :sogo_visible, `sogo_visible`= :sogo_visible,
`internal`= :internal, `internal`= :internal,
`sender_allowed`= :sender_allowed,
`active`= :active `active`= :active
WHERE `id` = :id"); WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
@@ -2696,6 +2711,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto, ':goto' => $goto,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active, ':active' => $active,
':id' => $is_now['id'] ':id' => $is_now['id']
)); ));
@@ -3043,6 +3059,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
if (!empty($is_now)) { if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
@@ -3052,6 +3070,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); (int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); (int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); (int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); (int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
@@ -3185,9 +3205,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (isset($_data['sender_acl'])) { if (isset($_data['sender_acl'])) {
// Get sender_acl items set by admin // Get sender_acl items set by admin
$current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
$sender_acl_admin = array_merge( $sender_acl_admin = array_merge(
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'], $current_sender_acls['sender_acl_domains']['ro'],
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro'] $current_sender_acls['sender_acl_addresses']['ro']
); );
// Get sender_acl items from POST array // Get sender_acl items from POST array
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset // Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
@@ -3275,16 +3296,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array( $stmt->execute(array(
':username' => $username ':username' => $username
)); ));
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases']; $sender_acl_handles = mailbox('get', 'sender_acl_handles', $username);
$fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
$fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
foreach ($sender_acl_merged as $sender_acl) { foreach ($sender_acl_merged as $sender_acl) {
$domain = ltrim($sender_acl, '@'); $domain = ltrim($sender_acl, '@');
if (is_valid_domain_name($domain)) { if (is_valid_domain_name($domain)) {
$sender_acl = '@' . $domain; $sender_acl = '@' . $domain;
} }
// Don't add if allowed by alias
if (in_array($sender_acl, $fixed_sender_aliases)) { // Always add to sender_acl table to create explicit permission
// Skip only if it's in allowed list (would be redundant)
// But DO add if it's in blocked list (creates override)
if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
// Skip: already allowed by sender_allowed=1, no need for sender_acl entry
continue; continue;
} }
// Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`) $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
VALUES (:sender_acl, :username)"); VALUES (:sender_acl, :username)");
$stmt->execute(array( $stmt->execute(array(
@@ -3335,6 +3365,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access), `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email), `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash) `attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
WHERE `username` = :username"); WHERE `username` = :username");
@@ -3349,6 +3381,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':pop3_access' => $pop3_access, ':pop3_access' => $pop3_access,
':sieve_access' => $sieve_access, ':sieve_access' => $sieve_access,
':smtp_access' => $smtp_access, ':smtp_access' => $smtp_access,
':eas_access' => $eas_access,
':dav_access' => $dav_access,
':recovery_email' => $pw_recovery_email, ':recovery_email' => $pw_recovery_email,
':relayhost' => $relayhost, ':relayhost' => $relayhost,
':username' => $username, ':username' => $username,
@@ -3731,6 +3765,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
foreach ($is_now as $key => $value){ foreach ($is_now as $key => $value){
@@ -4160,13 +4196,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$data['sender_acl_addresses']['rw'] = array(); $data['sender_acl_addresses']['rw'] = array();
$data['sender_acl_addresses']['selectable'] = array(); $data['sender_acl_addresses']['selectable'] = array();
$data['fixed_sender_aliases'] = array(); $data['fixed_sender_aliases'] = array();
$data['fixed_sender_aliases_allowed'] = array();
$data['fixed_sender_aliases_blocked'] = array();
$data['external_sender_aliases'] = array(); $data['external_sender_aliases'] = array();
// Fixed addresses // Fixed addresses - split by sender_allowed status
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'"); $stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)')); $stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) { while ($row = array_shift($rows)) {
// Keep old array for backward compatibility
$data['fixed_sender_aliases'][] = $row['address']; $data['fixed_sender_aliases'][] = $row['address'];
// Split into allowed/blocked for proper display
if ($row['sender_allowed'] == '1') {
$data['fixed_sender_aliases_allowed'][] = $row['address'];
} else {
$data['fixed_sender_aliases_blocked'][] = $row['address'];
}
} }
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain` $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain` WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
@@ -4726,6 +4771,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`internal`, `internal`,
`active`, `active`,
`sogo_visible`, `sogo_visible`,
`sender_allowed`,
`created`, `created`,
`modified` `modified`
FROM `alias` FROM `alias`
@@ -4759,6 +4805,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdata['active_int'] = $row['active']; $aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible']; $aliasdata['sogo_visible'] = $row['sogo_visible'];
$aliasdata['sogo_visible_int'] = $row['sogo_visible']; $aliasdata['sogo_visible_int'] = $row['sogo_visible'];
$aliasdata['sender_allowed'] = $row['sender_allowed'];
$aliasdata['created'] = $row['created']; $aliasdata['created'] = $row['created'];
$aliasdata['modified'] = $row['modified']; $aliasdata['modified'] = $row['modified'];
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) { if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {

View File

@@ -4,7 +4,7 @@ function init_db_schema()
try { try {
global $pdo; global $pdo;
$db_version = "10312025_0525"; $db_version = "28012026_1000";
$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));
@@ -185,6 +185,7 @@ function init_db_schema()
"public_comment" => "TEXT", "public_comment" => "TEXT",
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'", "internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'" "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
), ),
"keys" => array( "keys" => array(
@@ -1394,6 +1395,8 @@ function init_db_schema()
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.eas_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.eas_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.dav_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.dav_access') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;");
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;");

View File

@@ -121,7 +121,7 @@ class mailcowPdo extends OAuth2\Storage\Pdo {
$this->config['user_table'] = 'mailbox'; $this->config['user_table'] = 'mailbox';
} }
public function checkUserCredentials($username, $password) { public function checkUserCredentials($username, $password) {
if (check_login($username, $password) == 'user') { if (check_login($username, $password, array("role" => "user", "service" => "NONE")) == 'user') {
return true; return true;
} }
return false; return false;

View File

@@ -44,7 +44,7 @@ if (isset($_GET["cancel_tfa_login"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"])); $login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"], false, array("role" => "admin")); $as = check_login($login_user, $_POST["pass_user"], array("role" => "admin", "service" => "MAILCOWUI"));
if ($as == "admin") { if ($as == "admin") {
session_regenerate_id(true); session_regenerate_id(true);

View File

@@ -55,7 +55,7 @@ if (isset($_GET["cancel_tfa_login"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"])); $login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"], false, array("role" => "domain_admin")); $as = check_login($login_user, $_POST["pass_user"], array("role" => "domain_admin", "service" => "MAILCOWUI"));
if ($as == "domainadmin") { if ($as == "domainadmin") {
session_regenerate_id(true); session_regenerate_id(true);

View File

@@ -119,7 +119,7 @@ if (isset($_GET["cancel_tfa_login"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"])); $login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"], false, array("role" => "user")); $as = check_login($login_user, $_POST["pass_user"], array("role" => "user", "service" => "MAILCOWUI"));
if ($as == "user") { if ($as == "user") {
set_user_loggedin_session($login_user); set_user_loggedin_session($login_user);

View File

@@ -33,6 +33,8 @@ if ($https_port === FALSE) {
//$https_port = 1234; //$https_port = 1234;
// Other settings => // Other settings =>
$autodiscover_config = array( $autodiscover_config = array(
'displayName' => 'A mailcow mail server',
'displayShortName' => 'mail server',
// General autodiscover service type: "activesync" or "imap" // General autodiscover service type: "activesync" or "imap"
// emClient uses autodiscover, but does not support ActiveSync. mailcow excludes emClient from ActiveSync. // emClient uses autodiscover, but does not support ActiveSync. mailcow excludes emClient from ActiveSync.
// With SOGo disabled, the type will always fallback to imap. CalDAV and CardDAV will be excluded, too. // With SOGo disabled, the type will always fallback to imap. CalDAV and CardDAV will be excluded, too.
@@ -215,6 +217,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['smtp_access'] = true;
// Mailbox has sieve access by default // Mailbox has sieve access by default
$MAILBOX_DEFAULT_ATTRIBUTES['sieve_access'] = true; $MAILBOX_DEFAULT_ATTRIBUTES['sieve_access'] = true;
// Mailbox has ActiveSync/EAS access by default
$MAILBOX_DEFAULT_ATTRIBUTES['eas_access'] = true;
// Mailbox has CalDAV/CardDAV (DAV) access by default
$MAILBOX_DEFAULT_ATTRIBUTES['dav_access'] = true;
// Mailbox receives notifications about... // Mailbox receives notifications about...
// "add_header" - mail that was put into the Junk folder // "add_header" - mail that was put into the Junk folder
// "reject" - mail that was rejected // "reject" - mail that was rejected

View File

@@ -27,6 +27,12 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
exit(); exit();
} }
$host = strtolower($_SERVER['HTTP_HOST'] ?? '');
if (str_starts_with($host, 'autodiscover.') || str_starts_with($host, 'autoconfig.')) {
http_response_code(404);
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING']; $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];

View File

@@ -54,7 +54,16 @@ jQuery(function($){
$.get("/inc/ajax/show_rspamd_global_filters.php"); $.get("/inc/ajax/show_rspamd_global_filters.php");
$("#confirm_show_rspamd_global_filters").hide(); $("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("d-none"); $("#rspamd_global_filters").removeClass("d-none");
localStorage.setItem('rspamd_global_filters_confirmed', 'true');
}); });
$(document).ready(function() {
if (localStorage.getItem('rspamd_global_filters_confirmed') === 'true') {
$("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("d-none");
}
});
$("#super_delete").click(function() { return confirm(lang.queue_ays); }); $("#super_delete").click(function() { return confirm(lang.queue_ays); });
$(".refresh_table").on('click', function(e) { $(".refresh_table").on('click', function(e) {

View File

@@ -352,6 +352,12 @@ $(document).ready(function() {
if (template.sieve_access == 1){ if (template.sieve_access == 1){
protocol_access.push("sieve"); protocol_access.push("sieve");
} }
if (template.eas_access == 1){
protocol_access.push("eas");
}
if (template.dav_access == 1){
protocol_access.push("dav");
}
$('#protocol_access').selectpicker('val', protocol_access); $('#protocol_access').selectpicker('val', protocol_access);
var acl = []; var acl = [];
@@ -933,6 +939,8 @@ jQuery(function($){
item.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"></i>'; item.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"></i>'; item.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"></i>'; item.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
item.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
if (item.attributes.quarantine_notification === 'never') { if (item.attributes.quarantine_notification === 'never') {
item.quarantine_notification = lang.never; item.quarantine_notification = lang.never;
} else if (item.attributes.quarantine_notification === 'hourly') { } else if (item.attributes.quarantine_notification === 'hourly') {
@@ -1096,6 +1104,18 @@ jQuery(function($){
defaultContent: '', defaultContent: '',
className: 'none' className: 'none'
}, },
{
title: 'EAS',
data: 'eas_access',
defaultContent: '',
className: 'none'
},
{
title: 'DAV',
data: 'dav_access',
defaultContent: '',
className: 'none'
},
{ {
title: lang.quarantine_notification, title: lang.quarantine_notification,
data: 'quarantine_notification', data: 'quarantine_notification',
@@ -1209,6 +1229,8 @@ jQuery(function($){
item.attributes.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.imap_access == 1 ? '1' : '0') + '</span></i>'; item.attributes.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.imap_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.smtp_access == 1 ? '1' : '0') + '</span></i>'; item.attributes.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.smtp_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sieve_access == 1 ? '1' : '0') + '</span></i>'; item.attributes.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sieve_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.eas_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.dav_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>'; item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>';
if (item.attributes.quarantine_notification === 'never') { if (item.attributes.quarantine_notification === 'never') {
item.attributes.quarantine_notification = lang.never; item.attributes.quarantine_notification = lang.never;
@@ -1317,6 +1339,16 @@ jQuery(function($){
data: 'attributes.sieve_access', data: 'attributes.sieve_access',
defaultContent: '', defaultContent: '',
}, },
{
title: 'EAS',
data: 'attributes.eas_access',
defaultContent: '',
},
{
title: 'DAV',
data: 'attributes.dav_access',
defaultContent: '',
},
{ {
title: 'SOGO', title: 'SOGO',
data: 'attributes.sogo_access', data: 'attributes.sogo_access',

View File

@@ -1007,9 +1007,9 @@ if (isset($_GET['query'])) {
['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"], ['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"], ['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
['db' => 'name', 'dt' => 7], ['db' => 'name', 'dt' => 7],
['db' => 'messages', 'dt' => 18, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"], ['db' => 'messages', 'dt' => 20, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
['db' => 'tags', 'dt' => 20, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']], ['db' => 'tags', 'dt' => 23, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
['db' => 'active', 'dt' => 21], ['db' => 'active', 'dt' => 24],
]; ];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';

View File

@@ -73,6 +73,7 @@
"inactive": "Inaktiv", "inactive": "Inaktiv",
"internal": "Intern", "internal": "Intern",
"internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.", "internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
"sender_allowed": "Als dieser Alias senden erlauben",
"kind": "Art", "kind": "Art",
"mailbox_quota_def": "Standard-Quota einer Mailbox", "mailbox_quota_def": "Standard-Quota einer Mailbox",
"mailbox_quota_m": "Max. Speicherplatz pro Mailbox (MiB)", "mailbox_quota_m": "Max. Speicherplatz pro Mailbox (MiB)",
@@ -694,6 +695,8 @@
"inactive": "Inaktiv", "inactive": "Inaktiv",
"internal": "Intern", "internal": "Intern",
"internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.", "internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
"sender_allowed": "Als dieser Alias senden erlauben",
"sender_allowed_info": "Wenn deaktiviert, kann dieser Alias nur E-Mails empfangen. Verwenden Sie Sender-ACL, um bestimmten Postfächern die Berechtigung zum Senden zu erteilen.",
"kind": "Art", "kind": "Art",
"last_modified": "Zuletzt geändert", "last_modified": "Zuletzt geändert",
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)", "lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",

View File

@@ -73,6 +73,7 @@
"inactive": "Inactive", "inactive": "Inactive",
"internal": "Internal", "internal": "Internal",
"internal_info": "Internal aliases are only accessible from the own domain or alias domains.", "internal_info": "Internal aliases are only accessible from the own domain or alias domains.",
"sender_allowed": "Allow to send as this alias",
"kind": "Kind", "kind": "Kind",
"mailbox_quota_def": "Default mailbox quota", "mailbox_quota_def": "Default mailbox quota",
"mailbox_quota_m": "Max. quota per mailbox (MiB)", "mailbox_quota_m": "Max. quota per mailbox (MiB)",
@@ -694,6 +695,8 @@
"inactive": "Inactive", "inactive": "Inactive",
"internal": "Internal", "internal": "Internal",
"internal_info": "Internal aliases are only accessible from the own domain or alias domains.", "internal_info": "Internal aliases are only accessible from the own domain or alias domains.",
"sender_allowed": "Allow to send as this alias",
"sender_allowed_info": "If disabled, this alias can only receive mail. Use sender ACL to override and grant specific mailboxes permission to send.",
"kind": "Kind", "kind": "Kind",
"last_modified": "Last modified", "last_modified": "Last modified",
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*\\.google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)", "lookup_mx": "Destination is a regular expression to match against MX name (<code>.*\\.google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",

View File

@@ -1266,7 +1266,7 @@
"no_last_login": "Aucune dernière information de connexion à l'interface", "no_last_login": "Aucune dernière information de connexion à l'interface",
"no_record": "Pas d'enregistrement", "no_record": "Pas d'enregistrement",
"password": "Mot de passe", "password": "Mot de passe",
"password_now": "Mot de passe courant (confirmer les changements)", "password_now": "Mot de passe actuel (confirmer les changements)",
"password_repeat": "Mot de passe (répéter)", "password_repeat": "Mot de passe (répéter)",
"pushover_evaluate_x_prio": "Acheminement du courrier hautement prioritaire [<code>X-Priority: 1</code>]", "pushover_evaluate_x_prio": "Acheminement du courrier hautement prioritaire [<code>X-Priority: 1</code>]",
"pushover_info": "Les paramètres de notification push sappliqueront à tout le courrier propre (non spam) livré à <b>%s</b> y compris les alias (partagés, non partagés, étiquetés).", "pushover_info": "Les paramètres de notification push sappliqueront à tout le courrier propre (non spam) livré à <b>%s</b> y compris les alias (partagés, non partagés, étiquetés).",

View File

@@ -295,7 +295,9 @@
"user_quicklink": "Gyorshivatkozás elrejtése a Felhasználói bejelentkezési oldalra", "user_quicklink": "Gyorshivatkozás elrejtése a Felhasználói bejelentkezési oldalra",
"validate_license_now": "GUID érvényesítése a licenszszerverrel szemben", "validate_license_now": "GUID érvényesítése a licenszszerverrel szemben",
"yes": "&#10003;", "yes": "&#10003;",
"success": "Siker" "success": "Siker",
"login_page": "Belépő oldal",
"needs_restart": "újraindítást igényel"
}, },
"edit": { "edit": {
"active": "Aktív", "active": "Aktív",
@@ -1070,7 +1072,7 @@
"post_domain_add": "A \"sogo-mailcow\" SOGo konténert újra kell indítani egy új tartomány hozzáadása után!<br><br>Kiegészítésképpen a tartományok DNS-konfigurációját is felül kell vizsgálni. A DNS-konfiguráció jóváhagyása után indítsa újra az \"acme-mailcow\"-t, hogy automatikusan generáljon tanúsítványokat az új tartományhoz (autoconfig.&lt;domain&gt;, autodiscover.&lt;domain&gt;).<br>Ez a lépés opcionális, és 24 óránként megismétlődik.", "post_domain_add": "A \"sogo-mailcow\" SOGo konténert újra kell indítani egy új tartomány hozzáadása után!<br><br>Kiegészítésképpen a tartományok DNS-konfigurációját is felül kell vizsgálni. A DNS-konfiguráció jóváhagyása után indítsa újra az \"acme-mailcow\"-t, hogy automatikusan generáljon tanúsítványokat az új tartományhoz (autoconfig.&lt;domain&gt;, autodiscover.&lt;domain&gt;).<br>Ez a lépés opcionális, és 24 óránként megismétlődik.",
"dry": "Szinkronizálás szimulálása", "dry": "Szinkronizálás szimulálása",
"inactive": "Inaktív", "inactive": "Inaktív",
"kind": "Kedves", "kind": "Típus",
"mailbox_quota_m": "Maximális kvóta postafiókonként (MiB)", "mailbox_quota_m": "Maximális kvóta postafiókonként (MiB)",
"mailbox_username": "Felhasználónév (az e-mail cím bal oldali része)", "mailbox_username": "Felhasználónév (az e-mail cím bal oldali része)",
"max_aliases": "Max. lehetséges álnevek", "max_aliases": "Max. lehetséges álnevek",
@@ -1092,9 +1094,9 @@
"exclude": "Objektumok kizárása (regex)", "exclude": "Objektumok kizárása (regex)",
"full_name": "Teljes név", "full_name": "Teljes név",
"gal": "Globális címlista", "gal": "Globális címlista",
"goto_ham": "Tanulj <span class=\"text-success\"><b>sonkaként</b></span>", "goto_ham": "Tanítás <span class=\"text-success\"><b>valódi</b></span> levélként",
"goto_null": "Leveleket csendben eldobni", "goto_null": "Leveleket csendben eldobni",
"goto_spam": "Tanuld <span class=\"text-danger\"><b>spamként</b></span>", "goto_spam": "Tanítás <span class=\"text-danger\"><b>spam</b></span>ként",
"syncjob_hint": "Ne feledje, hogy a jelszavakat egyszerű szöveges formában kell elmenteni!", "syncjob_hint": "Ne feledje, hogy a jelszavakat egyszerű szöveges formában kell elmenteni!",
"target_address": "Továbbítási címek", "target_address": "Továbbítási címek",
"target_address_info": "<small>Teljes e-mail cím(ek) (vesszővel elválasztva).</small>", "target_address_info": "<small>Teljes e-mail cím(ek) (vesszővel elválasztva).</small>",
@@ -1102,7 +1104,7 @@
"comment_info": "A privát megjegyzés nem látható a felhasználó számára, míg a nyilvános megjegyzés tooltip-ként jelenik meg, amikor a felhasználó áttekintésében a megjegyzésre mutat.", "comment_info": "A privát megjegyzés nem látható a felhasználó számára, míg a nyilvános megjegyzés tooltip-ként jelenik meg, amikor a felhasználó áttekintésében a megjegyzésre mutat.",
"custom_params": "Egyéni paraméterek", "custom_params": "Egyéni paraméterek",
"gal_info": "A GAL tartalmazza a tartomány összes objektumát, és egyetlen felhasználó sem szerkesztheti. A SOGo-ban a Szabad/Elfoglalt információ hiányzik, ha ki van kapcsolva! <b>Indítsa újra a SOGo-t a változások alkalmazásához.</b>", "gal_info": "A GAL tartalmazza a tartomány összes objektumát, és egyetlen felhasználó sem szerkesztheti. A SOGo-ban a Szabad/Elfoglalt információ hiányzik, ha ki van kapcsolva! <b>Indítsa újra a SOGo-t a változások alkalmazásához.</b>",
"hostname": "Házigazda", "hostname": "Hoszt",
"backup_mx_options": "Továbbítási opciók", "backup_mx_options": "Továbbítási opciók",
"custom_params_hint": "Megfelelő: --param=xy, Rossz: --param xy", "custom_params_hint": "Megfelelő: --param=xy, Rossz: --param xy",
"delete1": "Törlés a forrásból, ha befejeződött", "delete1": "Törlés a forrásból, ha befejeződött",
@@ -1140,6 +1142,109 @@
"sieve_type": "Szűrő típusa", "sieve_type": "Szűrő típusa",
"skipcrossduplicates": "Duplikált üzenetek átugrása mappák között (érkezési sorrendben)", "skipcrossduplicates": "Duplikált üzenetek átugrása mappák között (érkezési sorrendben)",
"subscribeall": "Feliratkozás minden mappára", "subscribeall": "Feliratkozás minden mappára",
"syncjob": "Szinkronizálási feladat hozzáadása" "syncjob": "Szinkronizálási feladat hozzáadása",
"internal": "Belső",
"internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető."
},
"danger": {
"access_denied": "Hozzáférés megtagatva vagy nem megfelelő űrlap adat",
"alias_domain_invalid": "Az alias domain %s érvénytelen",
"alias_empty": "Az alias cím nem lehet üres",
"alias_goto_identical": "Az alias és a goto cím nem lehetnek azonosak",
"alias_invalid": "Az alias cím %s érvénytelen",
"aliasd_targetd_identical": "Az alias tartomány nem lehet azonos a céltartománnyal: %s",
"aliases_in_use": "A maximális aliasoknak nagyobbnak vagy egyenlőnek kell lenniük mint %d",
"app_name_empty": "Az alkalmazás neve nem lehet üres",
"app_passwd_id_invalid": "Alkalmazás jelszó ID %s érvénytelen",
"authsource_in_use": "A személyazonosság szolgáltatót nem lehet megváltoztatni vagy törölni, mivel ez jelenleg használatban van legalább 1 felhasználónál.",
"bcc_empty": "BCC cél nem lehet üres",
"bcc_exists": "A %s típushoz létezik egy %s típusú BCC térkép.",
"bcc_must_be_email": "A BCC cél %s nem érvényes e-mail cím",
"comment_too_long": "Túl hosszú megjegyzés, max 160 karakter megengedett",
"cors_invalid_method": "Érvénytelen Allow-Method lett megadva",
"cors_invalid_origin": "Érvénytelen Allow-Origin lett megadva",
"defquota_empty": "A postafiókonkénti alapértelmezett kvóta nem lehet 0.",
"demo_mode_enabled": "Demo mód engedélyezve",
"description_invalid": "A %s erőforrás leírása érvénytelen",
"dkim_domain_or_sel_exists": "A \"%s\" DKIM-kulcs létezik, és nem lesz felülírva",
"dkim_domain_or_sel_invalid": "DKIM tartomány vagy szelektor érvénytelen: %s",
"domain_cannot_match_hostname": "A tartomány nem egyezik a hostnévvel",
"domain_exists": "A %s domain már létezik",
"domain_invalid": "A domain név üres vagy érvénytelen",
"domain_not_empty": "Nem lehet eltávolítani a nem üres domaint %s",
"domain_not_found": "Nem található domain %s",
"domain_quota_m_in_use": "A domain kvótának nagyobbnak vagy egyenlőnek kell lennie %s MiB-nál",
"extended_sender_acl_denied": "hiányzó ACL külső küldő cím beállításához",
"extra_acl_invalid": "A \"%s\" külső feladó címe érvénytelen",
"extra_acl_invalid_domain": "Külső feladó \"%s\" érvénytelen tartományt használ",
"fido2_verification_failed": "FIDO2 ellenőrzés sikertelen: %s",
"file_open_error": "A fájl nem nyitható meg írásra",
"filter_type": "Rossz szűrőtípus",
"from_invalid": "A feladó nem lehet üres",
"generic_server_error": "Váratlan szerver hiba keletkezett. Vedd fel a kapcsolatot az adminisztrátorral.",
"global_filter_write_error": "Nem tudott szűrőfájlt írni: %s",
"global_map_invalid": "Globális térkép azonosítója %s érvénytelen",
"global_map_write_error": "Nem tudott globális térképet írni ID %s: %s",
"goto_empty": "Egy alias címnek legalább egy érvényes goto címet kell tartalmaznia.",
"goto_invalid": "Goto cím %s érvénytelen",
"ham_learn_error": "Ham tanulási hiba: %s",
"iam_test_connection": "Kapcsolódás sikertelen",
"imagick_exception": "Hiba: Kép olvasása közben Imagick hiba keletkezett",
"img_dimensions_exceeded": "A kép meghaladja a maximális méretet",
"img_invalid": "A képfájlt nem lehet érvényesíteni",
"img_size_exceeded": "A kép meghaladja a maximális fájl méretet",
"img_tmp_missing": "A képfájlt nem lehet érvényesíteni: Ideiglenes fájl nem található",
"invalid_bcc_map_type": "Érvénytelen a BCC térkép típusa",
"invalid_destination": "A \"%s\" célállomás formátum érvénytelen",
"invalid_filter_type": "Érvénytelen szűrőtípus",
"invalid_host": "Érvénytelen host megadva: %s",
"invalid_mime_type": "Érvénytelen mime típus",
"invalid_nexthop": "A következő ugrás formátuma érvénytelen",
"invalid_nexthop_authenticated": "A következő ugrás más hitelesítő adatokkal létezik, kérjük, először frissítse a meglévő hitelesítő adatokat ehhez a következő ugráshoz.",
"invalid_recipient_map_new": "Érvénytelen új címzett megadása: %s",
"invalid_recipient_map_old": "Érvénytelen eredeti címzett van megadva: %s",
"invalid_reset_token": "Érvénytelen visszaállító kulcs",
"ip_list_empty": "Az engedélyezett IP-k listája nem lehet üres",
"is_alias": "%s már ismert álnév címként",
"is_alias_or_mailbox": "%s már ismert alias, egy postafiók vagy egy alias tartományból kiterjesztett alias cím.",
"is_spam_alias": "%s már ismert ideiglenes alias cím (spam alias cím)",
"last_key": "Az utolsó kulcs nem törölhető, kérjük, helyette deaktiválja a TFA-t.",
"login_failed": "A bejelentkezés sikertelen",
"mailbox_defquota_exceeds_mailbox_maxquota": "Az alapértelmezett kvóta meghaladja a maximális kvótakorlátot",
"mailbox_invalid": "A postafiók neve érvénytelen",
"mailbox_quota_exceeded": "A kvóta meghaladja a tartományi korlátot (max. %d MiB)",
"mailbox_quota_exceeds_domain_quota": "A maximális kvóta meghaladja a tartományi kvótakorlátot",
"mailbox_quota_left_exceeded": "Nincs elég hely (maradék hely: %d MiB)",
"mailboxes_in_use": "A maximális postafiókoknak nagyobbnak vagy egyenlőnek kell lenniük %d-vel.",
"malformed_username": "Hibás felhasználónév",
"map_content_empty": "A térkép tartalma nem lehet üres",
"max_age_invalid": "Maximális kor %s érvénytelen",
"max_alias_exceeded": "Max. aliasok túllépése",
"max_mailbox_exceeded": "Max. postafiókok túllépése (%d %d-ből %d)",
"max_quota_in_use": "A postafiók kvótának nagyobbnak vagy egyenlőnek kell lennie %d MiB-nél",
"maxquota_empty": "A postafiókonkénti maximális kvóta nem lehet 0.",
"mode_invalid": "%s mód érvénytelen",
"mx_invalid": "%s MX rekord érvénytelen",
"mysql_error": "MySQL hiba: %s",
"network_host_invalid": "Érvénytelen hálózat vagy állomás: %s",
"next_hop_interferes": "%s zavarja a nexthop %s-t",
"next_hop_interferes_any": "Egy meglévő következő ugrás zavarja a %s-t.",
"nginx_reload_failed": "Az Nginx újratöltése sikertelen: %s",
"no_user_defined": "Nincs felhasználó által meghatározott",
"object_exists": "Az objektum %s már létezik",
"object_is_not_numeric": "Az érték %s nem numerikus",
"password_complexity": "A jelszó nem felel meg a szabályzatnak",
"password_empty": "A jelszó nem lehet üres",
"password_mismatch": "A megerősítő jelszó nem egyezik",
"password_reset_invalid_user": "A fiók nem található vagy nem lett megadva visszaállításhoz email cím",
"password_reset_na": "A jelszó visszaállítás jelenleg nem elérhető. Vedd fel a kapcsolatot az adminisztrátorral.",
"policy_list_from_exists": "A megadott nevű rekord létezik",
"policy_list_from_invalid": "A rekord érvénytelen formátumú",
"private_key_error": "Privát kulcs hiba: %s",
"pushover_credentials_missing": "Pushover token és/vagy kulcs hiányzik",
"pushover_key": "A pushover kulcs rossz formátumú",
"pushover_token": "A Pushover token rossz formátumú",
"quota_not_0_not_numeric": "A kvótának numerikusnak és >= 0-nak kell lennie.",
"recipient_map_entry_exists": "Létezik egy \"%s\" címzett-térkép bejegyzés"
} }
} }

View File

@@ -240,7 +240,7 @@
"generate": "Generuj", "generate": "Generuj",
"guid": "GUID - unikalny identyfikator instancji", "guid": "GUID - unikalny identyfikator instancji",
"guid_and_license": "GUID & licencja", "guid_and_license": "GUID & licencja",
"hash_remove_info": "Usunięcie hasha z limitem współczynnika (jeśli nadal istnieje) spowoduje całkowite zresetowanie jego licznika.<br>\n\n\n\n Każdy hash jest oznaczony indywidualnym kolorem.", "hash_remove_info": "Usunięcie hasha z limitem współczynnika (jeśli nadal istnieje) spowoduje całkowite zresetowanie jego licznika.<br> Każdy hash jest oznaczony indywidualnym kolorem.",
"help_text": "Zastąp tekst pomocy poniżej maski logowania (dozwolone HTML)", "help_text": "Zastąp tekst pomocy poniżej maski logowania (dozwolone HTML)",
"html": "HTML", "html": "HTML",
"iam": "Dostawca tożsamości", "iam": "Dostawca tożsamości",
@@ -675,7 +675,7 @@
"timeout1": "Limit czasu połączenia z serwerem zdalnym", "timeout1": "Limit czasu połączenia z serwerem zdalnym",
"timeout2": "Limit czasu połączenia z serwerem lokalnym", "timeout2": "Limit czasu połączenia z serwerem lokalnym",
"validate_save": "Zatwierdź i zapisz", "validate_save": "Zatwierdź i zapisz",
"pushover_info": "Ustawienia powiadomień push będą miały zastosowanie do wszystkich czystych (niespamowych) wiadomości dostarczanych do <b>%s</b>, w tym aliasów (współdzielonych, niewspółdzielonych, oznaczonych)", "pushover_info": "Ustawienia powiadomień push będą miały zastosowanie do wszystkich czystych (niespamowych) wiadomości dostarczanych do <b>%s</b> w tym aliasów (współdzielonych, niewspółdzielonych, oznaczonych).",
"mailbox_quota_def": "Domyślny limit skrzynki pocztowej", "mailbox_quota_def": "Domyślny limit skrzynki pocztowej",
"mailbox_relayhost_info": "Dotyczy wyłącznie skrzynki pocztowej i bezpośrednich aliasów, nadpisuje ustawienie serwera pośredniczącego (relayhost) dla domeny.", "mailbox_relayhost_info": "Dotyczy wyłącznie skrzynki pocztowej i bezpośrednich aliasów, nadpisuje ustawienie serwera pośredniczącego (relayhost) dla domeny.",
"maxbytespersecond": "Max. Ilość bajtów na sekundę <br><small>(0 = unlimited)</small>", "maxbytespersecond": "Max. Ilość bajtów na sekundę <br><small>(0 = unlimited)</small>",
@@ -683,7 +683,30 @@
"mailbox_rename_agree": "Stworzyłem kopię zapasową.", "mailbox_rename_agree": "Stworzyłem kopię zapasową.",
"mailbox_rename_warning": "WAŻNE! Utwórz kopię zapasową przed zmianą nazwy skrzynki pocztowej.", "mailbox_rename_warning": "WAŻNE! Utwórz kopię zapasową przed zmianą nazwy skrzynki pocztowej.",
"mailbox_rename_alias": "Tworzenie aliasów automatycznie", "mailbox_rename_alias": "Tworzenie aliasów automatycznie",
"mailbox_rename_title": "Nowa nazwa lokalnej skrzynki pocztowej" "mailbox_rename_title": "Nowa nazwa lokalnej skrzynki pocztowej",
"mbox_rl_info": "Ten limit szybkości dotyczy nazwy logowania SASL i odpowiada dowolnemu adresowi „from” używanemu przez zalogowanego użytkownika. Limit szybkości dla skrzynki pocztowej nadpisuje limit szybkości dla całej domeny.",
"nexthop": "Następny hop",
"private_comment": "Prywatny komentarz",
"public_comment": "Komentarz publiczny",
"mta_sts": "Konfiguruj MTA-STS",
"mta_sts_info": "<a\n\nhref='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> to standard wymuszający dostarczanie poczty elektronicznej pomiędzy serwerami pocztowymi z użyciem TLS oraz ważnych certyfikatów.\n\nJest stosowany wtedy, gdy użycie DANE nie jest możliwe z powodu braku lub nieobsługiwanego DNSSEC.\n\n<br><b>Uwaga</b>: Jeżeli domena odbiorcza obsługuje DANE z DNSSEC, DANE jest <b>zawsze</b> preferowane — MTA-STS działa wyłącznie jako mechanizm zapasowy.",
"mta_sts_version": "Wersja.",
"mta_sts_version_info": "Określa wersję standardu MTA-STS — obecnie jedyną prawidłową wartością jest <code>STSv1</code>..",
"mta_sts_mode": "Tryb.",
"mta_sts_mode_info": "Dostępne są trzy tryby do wyboru:\n<ul> <li><em>testing</em> polityka jest wyłącznie monitorowana, a naruszenia nie mają wpływu na dostarczanie poczty.</li> <li><em>enforce</em> polityka jest ściśle egzekwowana; połączenia bez ważnego TLS są odrzucane.</li> <li><em>none</em> polityka jest publikowana, lecz nie jest stosowana.</li> </ul>.",
"mta_sts_max_age": "Maksymalny czas obowiązywania.",
"mta_sts_max_age_info": "Czas (w sekundach) przechowywania polityki w cache przez serwery odbierające..",
"mta_sts_mx": "serwer MX.",
"mta_sts_mx_info": "Umożliwia wysyłanie poczty wyłącznie do jawnie wymienionych nazw hostów serwerów pocztowych; wysyłający MTA sprawdza, czy nazwa hosta MX w DNS odpowiada liście z polityki, i zezwala na dostarczenie tylko przy użyciu ważnego certyfikatu TLS (ochrona przed atakami MITM)..",
"mta_sts_mx_notice": "Dopuszcza się podanie wielu serwerów MX, rozdzielonych przecinkami..",
"none_inherit": "Brak /Dziedzicz",
"password_recovery_email": "Email do odzyskiwania hasła",
"pushover": "Pushover",
"pushover_evaluate_x_prio": "Eskaluj wiadomości o wysokim priorytecie [<code>X-Priority: 1</code>]",
"pushover_only_x_prio": "Uwzględniaj wyłącznie wiadomości o wysokim priorytecie [<code>X-Priority: 1</code>]",
"pushover_sender_array": "Uwzględniaj wyłącznie następujące adresy e-mail nadawców <small>(oddzielone przecinkami)</small>",
"pushover_sender_regex": "Bierz pod uwagę następujący regex nadawcy",
"pushover_text": "Tekst powiadomienia"
}, },
"footer": { "footer": {
"cancel": "Anuluj", "cancel": "Anuluj",
@@ -840,7 +863,14 @@
"template": "Szablon", "template": "Szablon",
"tls_map_dest": "Miejsce docelowe", "tls_map_dest": "Miejsce docelowe",
"tls_map_dest_info": "Przykłady: example.org, .example.org, [mail.example.org]:25", "tls_map_dest_info": "Przykłady: example.org, .example.org, [mail.example.org]:25",
"tls_map_parameters": "Parametry" "tls_map_parameters": "Parametry",
"add_recipient_map_entry": "Dodaj mapę odbiorców",
"add_template": "Dodaj szablon",
"add_tls_policy_map": "Dodaj mapę polityk TLS",
"address_rewriting": "Przepisywanie adresów",
"alias_domain_alias_hint": "Aliasy <b>nie</b> są automatycznie stosowane do aliasów domen. Adres aliasu <code>my-alias@domain</code> <b>nie</b> obejmuje adresu <code>my-alias@alias-domain</code> (gdzie „alias-domain” jest przykładową domeną aliasową dla „domain”).\n<br> Aby przekierować pocztę do zewnętrznej skrzynki, użyj filtra Sieve (zob. kartę „Filtry” lub SOGo → Przekazywanie). Skorzystaj z opcji „Rozszerz alias na domeny aliasowe”, aby automatycznie dodać brakujące aliasy.",
"alias_domain_backupmx": "Domena aliasowa nieaktywna dla domeny przekaźnikowej",
"all_domains": "Wszystkie domeny"
}, },
"quarantine": { "quarantine": {
"action": "Działanie", "action": "Działanie",
@@ -1075,7 +1105,7 @@
"spamfilter_table_remove": "Usuń", "spamfilter_table_remove": "Usuń",
"spamfilter_table_rule": "Zasada", "spamfilter_table_rule": "Zasada",
"spamfilter_wl": "Biała lista", "spamfilter_wl": "Biała lista",
"spamfilter_wl_desc": "Adresy e-mail znajdujące się na liście dozwolonych (allowlist) są zaprogramowane tak, aby <b> nigdy nie </b> były klasyfikowane jako spam.\nMożna używać symboli wieloznacznych (wildcardów).\nFiltr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów wskazujących na jedną skrzynkę pocztową), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki pocztowej", "spamfilter_wl_desc": "Adresy e-mail znajdujące się na liście dozwolonych (allowlist) są zaprogramowane tak, aby <b> nigdy nie </b> były klasyfikowane jako spam. Można używać symboli wieloznacznych (wildcardów).Filtr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów wskazujących na jedną skrzynkę pocztową), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki pocztowej",
"spamfilter_yellow": "Żółty: ta wiadomość może być spamem, zostanie oznaczona jako spam i przeniesiona do folderu spam", "spamfilter_yellow": "Żółty: ta wiadomość może być spamem, zostanie oznaczona jako spam i przeniesiona do folderu spam",
"sync_jobs": "Zadania synchronizacji", "sync_jobs": "Zadania synchronizacji",
"tag_handling": "Ustaw obsługę znaczników pocztowych", "tag_handling": "Ustaw obsługę znaczników pocztowych",
@@ -1175,7 +1205,8 @@
"waiting": "Oczekuje", "waiting": "Oczekuje",
"with_app_password": "z hasłem aplikacji", "with_app_password": "z hasłem aplikacji",
"year": "rok", "year": "rok",
"years": "lata" "years": "lata",
"spam_aliases_info": "Alias antyspamowy to tymczasowy adres e-mail, który może być używany do ochrony właściwych adresów pocztowych. <br>Opcjonalnie można ustawić czas wygaśnięcia, po którym alias zostanie automatycznie dezaktywowany, co pozwala skutecznie pozbyć się nadużywanych lub ujawnionych adresów."
}, },
"warning": { "warning": {
"session_ua": "Nieprawidłowy token formularza: Błąd walidacji User-Agent", "session_ua": "Nieprawidłowy token formularza: Błąd walidacji User-Agent",

View File

@@ -340,7 +340,8 @@
"tls_policy": "Política de TLS", "tls_policy": "Política de TLS",
"quarantine_attachments": "Anexos de quarentena", "quarantine_attachments": "Anexos de quarentena",
"filters": "Filtros", "filters": "Filtros",
"smtp_ip_access": "Mudar anfitriões permitidos para SMTP" "smtp_ip_access": "Mudar anfitriões permitidos para SMTP",
"app_passwds": "Gerenciar senhas de aplicativos"
}, },
"warning": { "warning": {
"no_active_admin": "Não é possível desactivar o último administrador activo" "no_active_admin": "Não é possível desactivar o último administrador activo"

View File

@@ -582,13 +582,13 @@
"username": "用户名", "username": "用户名",
"container_disabled": "容器已被停止或禁用", "container_disabled": "容器已被停止或禁用",
"container_running": "运行中", "container_running": "运行中",
"cores": "核心数", "cores": "核",
"memory": "内存", "memory": "内存",
"error_show_ip": "无法解析公网IP地址", "error_show_ip": "无法解析公网IP地址",
"show_ip": "显示公网IP", "show_ip": "显示公网IP",
"update_available": "有可用更新", "update_available": "有可用更新",
"update_failed": "无法检查更新", "update_failed": "无法检查更新",
"architecture": "构", "architecture": "构",
"container_stopped": "已停止", "container_stopped": "已停止",
"current_time": "系统时间", "current_time": "系统时间",
"timezone": "时区", "timezone": "时区",
@@ -1321,7 +1321,7 @@
"sogo_profile_reset": "重置 SOGo 个人资料", "sogo_profile_reset": "重置 SOGo 个人资料",
"sogo_profile_reset_help": "此操作会不可恢复地删除用户的 SOGo 个人资料并<b>删除所有联系人和日历数据</b>。", "sogo_profile_reset_help": "此操作会不可恢复地删除用户的 SOGo 个人资料并<b>删除所有联系人和日历数据</b>。",
"sogo_profile_reset_now": "立即重置个人资料", "sogo_profile_reset_now": "立即重置个人资料",
"spam_aliases": "临时邮箱别名", "spam_aliases": "垃圾邮件别名",
"spam_score_reset": "重置为服务器默认值", "spam_score_reset": "重置为服务器默认值",
"spamfilter": "垃圾邮件过滤器", "spamfilter": "垃圾邮件过滤器",
"spamfilter_behavior": "分数", "spamfilter_behavior": "分数",
@@ -1381,7 +1381,10 @@
"protocols": "协议", "protocols": "协议",
"authentication": "认证", "authentication": "认证",
"tfa_info": "两步验证有助于保护您的账户安全。启用后,对于不支持两步验证的应用程序或服务(例如邮件客户端),需要使用应用专用密码进行登录。", "tfa_info": "两步验证有助于保护您的账户安全。启用后,对于不支持两步验证的应用程序或服务(例如邮件客户端),需要使用应用专用密码进行登录。",
"overview": "概览" "overview": "概览",
"expire_never": "永不过期",
"forever": "永久",
"spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。<br>还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。"
}, },
"warning": { "warning": {
"cannot_delete_self": "不能删除已登录的用户", "cannot_delete_self": "不能删除已登录的用户",

View File

@@ -34,15 +34,15 @@ catch(PDOException $e) {
if (isset($_GET['only_email'])) { if (isset($_GET['only_email'])) {
$onlyEmailAccount = true; $onlyEmailAccount = true;
$description = 'IMAP'; $description = 'IMAP';
} else { } else {
$onlyEmailAccount = false; $onlyEmailAccount = false;
$description = 'IMAP, CalDAV, CardDAV'; $description = 'IMAP, CalDAV, CardDAV';
} }
if (isset($_GET['app_password'])) { if (isset($_GET['app_password'])) {
$app_password = true; $app_password = true;
$description .= ' with application password'; $description .= ' with application password';
if (strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') !== FALSE) if (strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') !== FALSE)
$platform = 'iPad'; $platform = 'iPad';
elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== FALSE) elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== FALSE)
@@ -51,8 +51,9 @@ if (isset($_GET['app_password'])) {
$platform = 'Mac'; $platform = 'Mac';
else else
$platform = $_SERVER['HTTP_USER_AGENT']; $platform = $_SERVER['HTTP_USER_AGENT'];
$password = bin2hex(openssl_random_pseudo_bytes(16)); $password = password_generate();
$attr = array( $attr = array(
'app_name' => $platform, 'app_name' => $platform,
'app_passwd' => $password, 'app_passwd' => $password,

View File

@@ -7,7 +7,30 @@ if (!isset($_SERVER['HTTP_HOST']) || strpos($_SERVER['HTTP_HOST'], 'mta-sts.') !
} }
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']); $host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
$domain = str_replace('mta-sts.', '', $host); $domain = idn_to_ascii(strtolower(str_replace('mta-sts.', '', $host)), 0, INTL_IDNA_VARIANT_UTS46);
// Validate domain or return 404 on error
if ($domain === false || empty($domain)) {
http_response_code(404);
exit;
}
// Check if domain is an alias domain and resolve to target domain
try {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$alias_row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($alias_row !== false && !empty($alias_row['target_domain'])) {
// This is an alias domain, use the target domain for MTA-STS lookup
$domain = $alias_row['target_domain'];
}
} catch (PDOException $e) {
// On database error, return 404
http_response_code(404);
exit;
}
$mta_sts = mailbox('get', 'mta_sts', $domain); $mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) == 0 || if (count($mta_sts) == 0 ||

View File

@@ -12,18 +12,21 @@ $session_var_pass = 'sogo-sso-pass';
if (isset($_SERVER['PHP_AUTH_USER'])) { if (isset($_SERVER['PHP_AUTH_USER'])) {
// load prerequisites only when required // load prerequisites only when required
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
$username = $_SERVER['PHP_AUTH_USER']; $username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW']; $password = $_SERVER['PHP_AUTH_PW'];
$is_eas = false;
$is_dav = false; // Determine service type for protocol access check
$service = 'NONE';
$original_uri = isset($_SERVER['HTTP_X_ORIGINAL_URI']) ? $_SERVER['HTTP_X_ORIGINAL_URI'] : ''; $original_uri = isset($_SERVER['HTTP_X_ORIGINAL_URI']) ? $_SERVER['HTTP_X_ORIGINAL_URI'] : '';
if (preg_match('/^(\/SOGo|)\/dav.*/', $original_uri) === 1) { if (preg_match('/^(\/SOGo|)\/dav.*/', $original_uri) === 1) {
$is_dav = true; $service = 'DAV';
} }
elseif (preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/', $original_uri) === 1) { elseif (preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/', $original_uri) === 1) {
$is_eas = true; $service = 'EAS';
} }
$login_check = check_login($username, $password, array('dav' => $is_dav, 'eas' => $is_eas));
$login_check = check_login($username, $password, array('service' => $service));
if ($login_check === 'user') { if ($login_check === 'user') {
header("X-User: $username"); header("X-User: $username");
header("X-Auth: Basic ".base64_encode("$username:$password")); header("X-Auth: Basic ".base64_encode("$username:$password"));
@@ -57,7 +60,6 @@ elseif (isset($_GET['login'])) {
$_SESSION['mailcow_cc_role'] = "user"; $_SESSION['mailcow_cc_role'] = "user";
} }
// update sasl logs // update sasl logs
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES ('SSO', 0, :username, :remote_addr)"); $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES ('SSO', 0, :username, :remote_addr)");
$stmt->execute(array( $stmt->execute(array(
':username' => $login, ':username' => $login,

View File

@@ -144,7 +144,7 @@
<form action="/" method="post" id="logout"><input type="hidden" name="logout"></form> <form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
{% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri %} {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri and mailcow_cc_username %}
<div class="container mt-4"> <div class="container mt-4">
<div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div> <div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div>
</div> </div>

View File

@@ -7,6 +7,7 @@
<form class="form-horizontal" data-id="editalias" role="form" method="post"> <form class="form-horizontal" data-id="editalias" role="form" method="post">
<input type="hidden" value="0" name="active"> <input type="hidden" value="0" name="active">
<input type="hidden" value="0" name="internal"> <input type="hidden" value="0" name="internal">
<input type="hidden" value="0" name="sender_allowed">
{% if not skip_sogo %} {% if not skip_sogo %}
<input type="hidden" value="0" name="sogo_visible"> <input type="hidden" value="0" name="sogo_visible">
{% endif %} {% endif %}
@@ -39,7 +40,11 @@
<div class="form-check"> <div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="internal"{% if result.internal == '1' %} checked{% endif %}> {{ lang.edit.internal }}</label> <label><input type="checkbox" class="form-check-input" value="1" name="internal"{% if result.internal == '1' %} checked{% endif %}> {{ lang.edit.internal }}</label>
</div> </div>
<small class="text-muted d-block">{{ lang.edit.internal_info }}</small> <small class="text-muted d-block mb-2">{{ lang.edit.internal_info }}</small>
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="sender_allowed"{% if result.sender_allowed == '1' %} checked{% endif %}> {{ lang.edit.sender_allowed }}</label>
</div>
<small class="text-muted d-block">{{ lang.edit.sender_allowed_info }}</small>
</div> </div>
</div> </div>
<hr> <hr>

View File

@@ -108,6 +108,8 @@
<option value="pop3"{% if template.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option> <option value="pop3"{% if template.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option>
<option value="smtp"{% if template.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option> <option value="smtp"{% if template.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option>
<option value="sieve"{% if template.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option> <option value="sieve"{% if template.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option>
<option value="eas"{% if template.attributes.eas_access == '1' %} selected{% endif %}>ActiveSync</option>
<option value="dav"{% if template.attributes.dav_access == '1' %} selected{% endif %}>CalDAV/CardDAV</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -85,14 +85,6 @@
{{ lang.edit.dont_check_sender_acl|format(domain) }} {{ lang.edit.dont_check_sender_acl|format(domain) }}
</option> </option>
{% endfor %} {% endfor %}
{% for alias in sender_acl_handles.sender_acl_addresses.ro %}
<option data-subtext="Admin" disabled selected>
{{ alias }}
</option>
{% endfor %}
{% for alias in sender_acl_handles.fixed_sender_aliases %}
<option data-subtext="Alias" disabled selected>{{ alias }}</option>
{% endfor %}
{% for domain in sender_acl_handles.sender_acl_domains.rw %} {% for domain in sender_acl_handles.sender_acl_domains.rw %}
<option value="{{ domain }}" selected> <option value="{{ domain }}" selected>
{{ lang.edit.dont_check_sender_acl|format(domain) }} {{ lang.edit.dont_check_sender_acl|format(domain) }}
@@ -104,11 +96,25 @@
</option> </option>
{% endfor %} {% endfor %}
{% for address in sender_acl_handles.sender_acl_addresses.rw %} {% for address in sender_acl_handles.sender_acl_addresses.rw %}
<option selected>{{ address }}</option> {% if address in sender_acl_handles.fixed_sender_aliases_allowed or address in sender_acl_handles.fixed_sender_aliases_blocked %}
<option data-subtext="Alias" selected>{{ address }}</option>
{% else %}
<option selected>{{ address }}</option>
{% endif %}
{% endfor %} {% endfor %}
{% for address in sender_acl_handles.sender_acl_addresses.selectable %} {% for address in sender_acl_handles.sender_acl_addresses.selectable %}
<option>{{ address }}</option> <option>{{ address }}</option>
{% endfor %} {% endfor %}
{% for alias in sender_acl_handles.fixed_sender_aliases_allowed %}
{% if alias not in sender_acl_handles.sender_acl_addresses.rw %}
<option data-subtext="Alias (allowed)" value="{{ alias }}" selected>{{ alias }}</option>
{% endif %}
{% endfor %}
{% for alias in sender_acl_handles.fixed_sender_aliases_blocked %}
{% if alias not in sender_acl_handles.sender_acl_addresses.rw %}
<option data-subtext="Alias (blocked)" value="{{ alias }}">{{ alias }}</option>
{% endif %}
{% endfor %}
</select> </select>
<div id="sender_acl_disabled"><i class="bi bi-shield-exclamation"></i> {{ lang.edit.sender_acl_disabled|raw }}</div> <div id="sender_acl_disabled"><i class="bi bi-shield-exclamation"></i> {{ lang.edit.sender_acl_disabled|raw }}</div>
<small class="text-muted d-block">{{ lang.edit.sender_acl_info|raw }}</small> <small class="text-muted d-block">{{ lang.edit.sender_acl_info|raw }}</small>
@@ -281,6 +287,8 @@
<option value="pop3"{% if result.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option> <option value="pop3"{% if result.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option>
<option value="smtp"{% if result.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option> <option value="smtp"{% if result.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option>
<option value="sieve"{% if result.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option> <option value="sieve"{% if result.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option>
<option value="eas"{% if result.attributes.eas_access == '1' %} selected{% endif %}>ActiveSync</option>
<option value="dav"{% if result.attributes.dav_access == '1' %} selected{% endif %}>CalDAV/CardDAV</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -148,6 +148,8 @@
<option value="pop3">POP3</option> <option value="pop3">POP3</option>
<option value="smtp">SMTP</option> <option value="smtp">SMTP</option>
<option value="sieve">Sieve</option> <option value="sieve">Sieve</option>
<option value="eas">ActiveSync</option>
<option value="dav">CalDAV/CardDAV</option>
</select> </select>
</div> </div>
</div> </div>
@@ -335,6 +337,8 @@
<option value="pop3" selected>POP3</option> <option value="pop3" selected>POP3</option>
<option value="smtp" selected>SMTP</option> <option value="smtp" selected>SMTP</option>
<option value="sieve" selected>Sieve</option> <option value="sieve" selected>Sieve</option>
<option value="activesync" selected>ActiveSync</option>
<option value="dav" selected>CalDAV/CardDAV</option>
</select> </select>
</div> </div>
</div> </div>
@@ -778,6 +782,7 @@
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias"> <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias">
<input type="hidden" value="0" name="active"> <input type="hidden" value="0" name="active">
<input type="hidden" value="0" name="internal"> <input type="hidden" value="0" name="internal">
<input type="hidden" value="0" name="sender_allowed">
<div class="row mb-2"> <div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="address">{{ lang.add.alias_address }}</label> <label class="control-label col-sm-2 text-sm-end" for="address">{{ lang.add.alias_address }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
@@ -809,7 +814,11 @@
<div class="form-check"> <div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="internal"> {{ lang.add.internal }}</label> <label><input type="checkbox" class="form-check-input" value="1" name="internal"> {{ lang.add.internal }}</label>
</div> </div>
<small class="text-muted d-block">{{ lang.edit.internal_info }}</small> <small class="text-muted d-block mb-2">{{ lang.edit.internal_info }}</small>
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="sender_allowed" checked> {{ lang.add.sender_allowed }}</label>
</div>
<small class="text-muted d-block">{{ lang.edit.sender_allowed_info }}</small>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">

View File

@@ -55,6 +55,8 @@
{% if mailboxdata.attributes.smtp_access == 1 %}<div class="badge fs-6 bg-success m-2">SMTP <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">SMTP <i class="bi bi-x-lg"></i></div>{% endif %} {% if mailboxdata.attributes.smtp_access == 1 %}<div class="badge fs-6 bg-success m-2">SMTP <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">SMTP <i class="bi bi-x-lg"></i></div>{% endif %}
{% if mailboxdata.attributes.sieve_access == 1 %}<div class="badge fs-6 bg-success m-2">Sieve <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">Sieve <i class="bi bi-x-lg"></i></div>{% endif %} {% if mailboxdata.attributes.sieve_access == 1 %}<div class="badge fs-6 bg-success m-2">Sieve <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">Sieve <i class="bi bi-x-lg"></i></div>{% endif %}
{% if mailboxdata.attributes.pop3_access == 1 %}<div class="badge fs-6 bg-success m-2">POP3 <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">POP3 <i class="bi bi-x-lg"></i></div>{% endif %} {% if mailboxdata.attributes.pop3_access == 1 %}<div class="badge fs-6 bg-success m-2">POP3 <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">POP3 <i class="bi bi-x-lg"></i></div>{% endif %}
{% if mailboxdata.attributes.eas_access == 1 %}<div class="badge fs-6 bg-success m-2">ActiveSync <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">ActiveSync <i class="bi bi-x-lg"></i></div>{% endif %}
{% if mailboxdata.attributes.dav_access == 1 %}<div class="badge fs-6 bg-success m-2">CalDAV/CardDAV <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">CalDAV/CardDAV <i class="bi bi-x-lg"></i></div>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -84,7 +84,7 @@ services:
- clamd - clamd
rspamd-mailcow: rspamd-mailcow:
image: ghcr.io/mailcow/rspamd:2.4 image: ghcr.io/mailcow/rspamd:3.14.2
stop_grace_period: 30s stop_grace_period: 30s
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow
@@ -117,7 +117,7 @@ services:
- rspamd - rspamd
php-fpm-mailcow: php-fpm-mailcow:
image: ghcr.io/mailcow/phpfpm:8.2.29 image: ghcr.io/mailcow/phpfpm:8.2.29-1
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on: depends_on:
- redis-mailcow - redis-mailcow
@@ -200,7 +200,7 @@ services:
- phpfpm - phpfpm
sogo-mailcow: sogo-mailcow:
image: ghcr.io/mailcow/sogo:5.12.4 image: ghcr.io/mailcow/sogo:5.12.4-1
environment: environment:
- DBNAME=${DBNAME} - DBNAME=${DBNAME}
- DBUSER=${DBUSER} - DBUSER=${DBUSER}
@@ -252,7 +252,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: ghcr.io/mailcow/dovecot:2.3.21.1 image: ghcr.io/mailcow/dovecot:2.3.21.1-1
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
- netfilter-mailcow - netfilter-mailcow
@@ -321,7 +321,7 @@ services:
ofelia.job-exec.dovecot_clean_q_aged.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/clean_q_aged.sh || exit 0\"" ofelia.job-exec.dovecot_clean_q_aged.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/clean_q_aged.sh || exit 0\""
ofelia.job-exec.dovecot_maildir_gc.schedule: "0 */30 * * * *" ofelia.job-exec.dovecot_maildir_gc.schedule: "0 */30 * * * *"
ofelia.job-exec.dovecot_maildir_gc.command: "/bin/bash -c \"source /source_env.sh ; /usr/local/bin/gosu vmail /usr/local/bin/maildir_gc.sh\"" ofelia.job-exec.dovecot_maildir_gc.command: "/bin/bash -c \"source /source_env.sh ; /usr/local/bin/gosu vmail /usr/local/bin/maildir_gc.sh\""
ofelia.job-exec.dovecot_sarules.schedule: "0 0 0 * * *" ofelia.job-exec.dovecot_sarules.schedule: "@every 24h"
ofelia.job-exec.dovecot_sarules.command: "/bin/bash -c \"/usr/local/bin/sa-rules.sh\"" ofelia.job-exec.dovecot_sarules.command: "/bin/bash -c \"/usr/local/bin/sa-rules.sh\""
ofelia.job-exec.dovecot_fts.schedule: "0 0 0 * * *" ofelia.job-exec.dovecot_fts.schedule: "0 0 0 * * *"
ofelia.job-exec.dovecot_fts.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/optimize-fts.sh\"" ofelia.job-exec.dovecot_fts.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/optimize-fts.sh\""
@@ -339,7 +339,7 @@ services:
- dovecot - dovecot
postfix-mailcow: postfix-mailcow:
image: ghcr.io/mailcow/postfix:3.7.11 image: ghcr.io/mailcow/postfix:3.7.11-1
depends_on: depends_on:
mysql-mailcow: mysql-mailcow:
condition: service_started condition: service_started
@@ -465,7 +465,7 @@ services:
condition: service_started condition: service_started
unbound-mailcow: unbound-mailcow:
condition: service_healthy condition: service_healthy
image: ghcr.io/mailcow/acme:1.94 image: ghcr.io/mailcow/acme:1.95
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
environment: environment:

View File

@@ -186,13 +186,13 @@ DBNAME=mailcow
DBUSER=mailcow DBUSER=mailcow
# Please use long, random alphanumeric strings (A-Za-z0-9) # Please use long, random alphanumeric strings (A-Za-z0-9)
DBPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) DBPASS=${MAILCOW_DBPASS:-$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28)}
DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) DBROOT=${MAILCOW_DBROOT:-$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28)}
# ------------------------------ # ------------------------------
# REDIS configuration # REDIS configuration
# ------------------------------ # ------------------------------
REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) REDISPASS=${MAILCOW_REDISPASS:-$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28)}
# ------------------------------ # ------------------------------
# HTTP/S Bindings # HTTP/S Bindings

View File

@@ -91,6 +91,44 @@ if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
exit 1 exit 1
fi fi
# Add image prefetch function
function prefetch_image() {
echo "Checking Docker image: ${DEBIAN_DOCKER_IMAGE}"
# Get local image digest if it exists
local local_digest=$(docker image inspect ${DEBIAN_DOCKER_IMAGE} --format='{{index .RepoDigests 0}}' 2>/dev/null | cut -d'@' -f2)
# Get remote image digest without pulling
local remote_digest=$(docker manifest inspect ${DEBIAN_DOCKER_IMAGE} 2>/dev/null | grep -oP '"digest":\s*"\K[^"]+' | head -1)
if [[ -z "${remote_digest}" ]]; then
echo "Warning: Unable to check remote image"
if [[ -n "${local_digest}" ]]; then
echo "Using cached version"
echo
return 0
else
echo "Error: Image ${DEBIAN_DOCKER_IMAGE} not found locally or remotely"
exit 1
fi
fi
if [[ "${local_digest}" != "${remote_digest}" ]]; then
echo "Image update available, pulling ${DEBIAN_DOCKER_IMAGE}"
if docker pull ${DEBIAN_DOCKER_IMAGE} 2>/dev/null; then
echo "Successfully pulled ${DEBIAN_DOCKER_IMAGE}"
else
echo "Error: Failed to pull ${DEBIAN_DOCKER_IMAGE}"
exit 1
fi
else
echo "Image is up to date (${remote_digest:0:12}...)"
fi
echo
}
# Prefetch the image early in the script
prefetch_image
function backup() { function backup() {
DATE=$(date +"%Y-%m-%d-%H-%M-%S") DATE=$(date +"%Y-%m-%d-%H-%M-%S")