1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-06-14 18:40:23 +00:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Dmitriy Alekseev 156cfbeb4d Update Redis connection setup in postfix-tlspol.sh
Refactor Redis connection handling and configuration.
2025-11-13 22:15:01 +01:00
Josh 0413d26855 Allow making spam aliases permanent (#6888)
* Allow making spam aliases permanent

* added german translation

* updated Spamalias Twig + Rename in Spam Alias

* compose: update image tags to align to vendor version

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-11-13 16:05:01 +01:00
Patrik Kernstock 7b29c1f304 Disable nginx server_tokens in http context (#6873) 2025-11-13 15:19:11 +01:00
Patrik Kernstock ae3ef391ee Remove deprecated 'X-XSS-Protection' header (#6871) 2025-11-13 15:16:44 +01:00
Peter 7313f996d3 Update to trixie (#6907) 2025-11-13 15:16:00 +01:00
DerLinkman 62d16c9e56 compose: changes cronjobs to regular cron syntax + fixed sogo creds for cronjobs (#6866)
* cron: restructure cron timer to time on second (instead of random)

* dovecot: fix clearance for cron.creds file
2025-11-13 14:59:49 +01:00
DerLinkman 674b41ce08 updated the Contributing Guidelines 2025-11-12 10:16:54 +01:00
Claas Flint 1b833be760 Replace pigz with zstd for backup compression (#6897)
* Replace pigz with zstd for backup compression

This change replaces pigz (parallel gzip) with zstd (Zstandard) as the
compression algorithm for mailcow backups while maintaining full backward
compatibility with existing .tar.gz backups.

Benefits:
- Better compression ratios (12-37% improvement in tests)
- Improved compression speed with modern algorithm
- Maintains rsyncable functionality for incremental backups
- Full backward compatibility for restoring old .tar.gz backups
- Wide industry adoption and active development

Changes:
- Backup compression: pigz --rsyncable -p → zstd --rsyncable -T
- Backup decompression: pigz -d -p → zstd -d -T
- File extensions: .tar.gz → .tar.zst
- Added get_archive_info() function for intelligent format detection
- Updated backup Dockerfile to install zstd alongside pigz
- Restore function now auto-detects and handles both formats
- Updated FILE_SELECTION regex to recognize both .tar.zst and .tar.gz
- Updated comments to reflect new file extension

Backward Compatibility:
- Restore automatically detects .tar.zst (preferred) or .tar.gz (legacy)
- Existing .tar.gz backups can still be restored without issues
- pigz remains installed in backup image for legacy support
- Graceful fallback if backup file format not found

Testing:
- Added comprehensive test suite (test_backup_and_restore.sh)
- 12 automated tests covering all scenarios:
  * Backup creation (both formats)
  * Restore (both formats)
  * Format detection and priority
  * Error handling (missing files, empty dirs)
  * Content integrity verification
  * Multi-threading configuration
  * Large file compression (8.59 MB realistic data)

Test Results:
✓ zstd compression working
✓ pigz compression working (legacy)
✓ zstd decompression working
✓ pigz decompression working (backward compatible)
✓ Archive detection working
✓ Content integrity verified
✓ Format priority correct (.tar.zst preferred)
✓ Error handling for missing files
✓ Error handling for empty directories
✓ Multi-threading configuration verified
✓ Large file compression: 37.05% improvement
✓ Small file compression: 12.18% improvement

* move testing script into development folder

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-11-12 10:06:36 +01:00
DerLinkman 88adb1adf5 remove dev docker volume from upstream 2025-11-12 09:54:35 +01:00
DerLinkman ec472f13cf sogo: removed URLDecrpytion by default, make it configurable in sogo.conf 2025-11-12 09:50:41 +01:00
milkmaker 2e1d98cc7c [Web] Updated lang.pl-pl.json (#6908)
[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-10 21:06:13 +01:00
milkmaker 07d7e3dc30 [Web] Updated lang.pl-pl.json (#6906)
[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-09 23:03:00 +01:00
milkmaker b0f5aee628 [Web] Updated lang.pl-pl.json (#6898)
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-05 17:37:26 +01:00
milkmaker d3065612fd update postscreen_access.cidr (#6886) 2025-11-03 21:07:40 +01:00
Josh 9912e41f78 [Web] Correct order of Dansk/Danish in UI (#6887) 2025-11-03 21:07:20 +01:00
milkmaker 04200c99a4 Translations update from Weblate (#6880)
* [Web] Updated lang.vi-vn.json

Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.nb-no.json

Co-authored-by: Runar Ingebrigtsen <runar@rin.no>

---------

Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
Co-authored-by: Runar Ingebrigtsen <runar@rin.no>
2025-10-27 20:00:29 +01:00
26 changed files with 1356 additions and 230 deletions
+7 -3
View File
@@ -1,11 +1,11 @@
# Contribution Guidelines # Contribution Guidelines
**_Last modified on 15th August 2024_** **_Last modified on 12th November 2025_**
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly. As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request. **PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
## Topics ## Topics
@@ -27,14 +27,18 @@ However, please note the following regarding pull requests:
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* 6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. 7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! 8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
--- ---
## Issue Reporting ## Issue Reporting
**_Last modified on 15th August 2024_** **_Last modified on 12th November 2025_**
If you plan to report a issue within mailcow please read and understand the following rules: If you plan to report a issue within mailcow please read and understand the following rules:
### Security disclosures / Security-related fixes
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
### Issue Reporting Guidelines ### Issue Reporting Guidelines
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). 1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
+2 -2
View File
@@ -1,3 +1,3 @@
FROM debian:bookworm-slim FROM debian:trixie-slim
RUN apt update && apt install pigz -y --no-install-recommends RUN apt update && apt install pigz zstd -y --no-install-recommends
@@ -204,16 +204,17 @@ EOF
# Create random master Password for SOGo SSO # Create random master Password for SOGo SSO
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
# Creating additional creds file for SOGo notify crons (calendars, etc)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
cat <<EOF > /etc/dovecot/sogo-sso.conf cat <<EOF > /etc/dovecot/sogo-sso.conf
# Autogenerated by mailcow # Autogenerated by mailcow
passdb { passdb {
driver = static driver = static
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
} }
EOF EOF
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
cat <<'EOF' > /usr/local/bin/quota_notify.py cat <<'EOF' > /usr/local/bin/quota_notify.py
+1 -1
View File
@@ -167,7 +167,7 @@ DELIMITER //
CREATE EVENT clean_spamalias CREATE EVENT clean_spamalias
ON SCHEDULE EVERY 1 DAY DO ON SCHEDULE EVERY 1 DAY DO
BEGIN BEGIN
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP(); DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
END; END;
// //
DELIMITER ; DELIMITER ;
@@ -18,11 +18,15 @@ done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" export REDIS_SERVER="${REDIS_SLAVEOF_IP}"
export REDIS_PORT="${REDIS_SLAVEOF_PORT}"
else else
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" export REDIS_SERVER="redis"
export REDIS_PORT="6379"
fi fi
export REDIS_CMDLINE="redis-cli -h ${REDIS_SERVER} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Redis..."
sleep 2 sleep 2
@@ -37,16 +41,13 @@ echo "Postfix OK"
cat <<EOF > /etc/postfix-tlspol/config.yaml cat <<EOF > /etc/postfix-tlspol/config.yaml
server: server:
address: 0.0.0.0:8642 address: 0.0.0.0:8642
log-level: ${LOGLVL} log-level: ${LOGLVL}
prefetch: true prefetch: true
cache-file: /var/lib/postfix-tlspol/cache.db
dns: dns:
# must support DNSSEC
address: 127.0.0.11:53 address: 127.0.0.11:53
redis:
address: ${REDIS_SERVER}:${REDIS_PORT}
db: 2
EOF EOF
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml /usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
+1 -1
View File
@@ -390,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME} dbname = ${DBNAME}
query = SELECT goto FROM spamalias query = SELECT goto FROM spamalias
WHERE address='%s' WHERE address='%s'
AND validity >= UNIX_TIMESTAMP() AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
EOF EOF
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
-4
View File
@@ -50,10 +50,6 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string> <string>YES</string>
<key>SOGoEncryptionKey</key> <key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string> <string>${RAND_PASS}</string>
<key>SOGoURLEncryptionEnabled</key>
<string>YES</string>
<key>SOGoURLEncryptionPassphrase</key>
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
<key>OCSAdminURL</key> <key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key> <key>OCSCacheFolderURL</key>
+1
View File
@@ -13,6 +13,7 @@ events {
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
server_tokens off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
@@ -14,7 +14,6 @@ ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=15768000;"; add_header Strict-Transport-Security "max-age=15768000;";
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none; add_header X-Robots-Tag none;
add_header X-Download-Options noopen; add_header X-Download-Options noopen;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
+8 -63
View File
@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025 # Whitelist generated by Postwhite v3.4 on Sat Nov 1 00:21:43 UTC 2025
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2216 total rules # 2161 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
@@ -50,14 +50,11 @@
8.25.194.0/23 permit 8.25.194.0/23 permit
8.25.196.0/23 permit 8.25.196.0/23 permit
8.36.116.0/24 permit 8.36.116.0/24 permit
8.39.54.0/23 permit
8.39.54.250/31 permit
8.39.144.0/24 permit 8.39.144.0/24 permit
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit 12.130.86.238 permit
13.107.213.41 permit 13.107.213.69 permit
13.107.246.41 permit 13.107.246.69 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
13.110.216.0/22 permit 13.110.216.0/22 permit
@@ -169,7 +166,6 @@
34.215.104.144 permit 34.215.104.144 permit
34.218.115.239 permit 34.218.115.239 permit
34.225.212.172 permit 34.225.212.172 permit
34.241.242.183 permit
35.83.148.184 permit 35.83.148.184 permit
35.155.198.111 permit 35.155.198.111 permit
35.158.23.94 permit 35.158.23.94 permit
@@ -193,7 +189,6 @@
40.233.64.216 permit 40.233.64.216 permit
40.233.83.78 permit 40.233.83.78 permit
40.233.88.28 permit 40.233.88.28 permit
43.239.212.33 permit
44.206.138.57 permit 44.206.138.57 permit
44.210.169.44 permit 44.210.169.44 permit
44.217.45.156 permit 44.217.45.156 permit
@@ -275,7 +270,6 @@
50.112.246.219 permit 50.112.246.219 permit
52.1.14.157 permit 52.1.14.157 permit
52.5.230.59 permit 52.5.230.59 permit
52.6.74.205 permit
52.12.53.23 permit 52.12.53.23 permit
52.13.214.179 permit 52.13.214.179 permit
52.26.1.71 permit 52.26.1.71 permit
@@ -302,7 +296,6 @@
52.96.91.34 permit 52.96.91.34 permit
52.96.111.82 permit 52.96.111.82 permit
52.96.172.98 permit 52.96.172.98 permit
52.96.214.50 permit
52.96.222.194 permit 52.96.222.194 permit
52.96.222.226 permit 52.96.222.226 permit
52.96.223.2 permit 52.96.223.2 permit
@@ -341,7 +334,6 @@
54.244.54.130 permit 54.244.54.130 permit
54.244.242.0/24 permit 54.244.242.0/24 permit
54.255.61.23 permit 54.255.61.23 permit
56.124.6.228 permit
57.103.64.0/18 permit 57.103.64.0/18 permit
57.129.93.249 permit 57.129.93.249 permit
62.13.128.0/24 permit 62.13.128.0/24 permit
@@ -426,7 +418,6 @@
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
65.154.166.0/24 permit
65.212.180.36 permit 65.212.180.36 permit
66.102.0.0/20 permit 66.102.0.0/20 permit
66.119.150.192/26 permit 66.119.150.192/26 permit
@@ -1233,8 +1224,6 @@
99.83.190.102 permit 99.83.190.102 permit
103.9.96.0/22 permit 103.9.96.0/22 permit
103.28.42.0/24 permit 103.28.42.0/24 permit
103.84.217.238 permit
103.89.75.238 permit
103.151.192.0/23 permit 103.151.192.0/23 permit
103.168.172.128/27 permit 103.168.172.128/27 permit
103.237.104.0/22 permit 103.237.104.0/22 permit
@@ -1400,9 +1389,6 @@
117.120.16.0/21 permit 117.120.16.0/21 permit
119.42.242.52/31 permit 119.42.242.52/31 permit
119.42.242.156 permit 119.42.242.156 permit
121.244.91.48 permit
121.244.91.52 permit
122.15.156.182 permit
123.126.78.64/29 permit 123.126.78.64/29 permit
124.108.96.24/31 permit 124.108.96.24/31 permit
124.108.96.28/31 permit 124.108.96.28/31 permit
@@ -1430,6 +1416,7 @@
128.245.248.0/21 permit 128.245.248.0/21 permit
129.41.77.70 permit 129.41.77.70 permit
129.41.169.249 permit 129.41.169.249 permit
129.77.16.0/20 permit
129.80.5.164 permit 129.80.5.164 permit
129.80.64.36 permit 129.80.64.36 permit
129.80.67.121 permit 129.80.67.121 permit
@@ -1466,21 +1453,8 @@
134.170.141.64/26 permit 134.170.141.64/26 permit
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.80.0/24 permit
135.84.81.0/24 permit
135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit 135.84.216.0/22 permit
136.143.160.0/24 permit 136.146.128.0/20 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.147.128.0/20 permit 136.147.128.0/20 permit
136.147.135.0/24 permit 136.147.135.0/24 permit
136.147.176.0/20 permit 136.147.176.0/20 permit
@@ -1495,7 +1469,6 @@
139.138.46.219 permit 139.138.46.219 permit
139.138.57.55 permit 139.138.57.55 permit
139.138.58.119 permit 139.138.58.119 permit
139.167.79.86 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.159.229 permit 141.148.159.229 permit
@@ -1618,9 +1591,6 @@
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
165.173.128.0/24 permit
165.173.180.250/31 permit
165.173.182.250/31 permit
166.78.68.0/22 permit 166.78.68.0/22 permit
166.78.68.221 permit 166.78.68.221 permit
166.78.69.169 permit 166.78.69.169 permit
@@ -1650,23 +1620,12 @@
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
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.138.0/24 permit
169.148.142.10 permit
169.148.142.33 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
169.148.175.3 permit
169.148.188.0/24 permit
169.148.188.182 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
170.10.132.64/29 permit 170.10.132.64/29 permit
170.10.133.0/24 permit 170.10.133.0/24 permit
172.217.32.0/20 permit 172.217.32.0/21 permit
172.253.56.0/21 permit 172.253.56.0/21 permit
172.253.112.0/20 permit 172.253.112.0/20 permit
173.0.84.0/29 permit 173.0.84.0/29 permit
@@ -1846,16 +1805,7 @@
199.16.156.0/22 permit 199.16.156.0/22 permit
199.33.145.1 permit 199.33.145.1 permit
199.33.145.32 permit 199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit 199.59.148.0/22 permit
199.67.80.2 permit
199.67.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.67.90.0/24 permit
199.101.161.130 permit 199.101.161.130 permit
199.101.162.0/25 permit 199.101.162.0/25 permit
199.122.120.0/21 permit 199.122.120.0/21 permit
@@ -1912,8 +1862,6 @@
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
204.141.32.0/23 permit
204.141.42.0/23 permit
204.216.164.202 permit 204.216.164.202 permit
204.220.160.0/21 permit 204.220.160.0/21 permit
204.220.168.0/21 permit 204.220.168.0/21 permit
@@ -2201,9 +2149,6 @@
2603:1030:20e:3::23c permit 2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit 2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit 2603:1030:c02:8::14 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
2607:f8b0:4000::/36 permit 2607:f8b0:4000::/36 permit
2620:109:c003:104::/64 permit 2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit 2620:109:c003:104::215 permit
+6
View File
@@ -86,6 +86,12 @@
SOGoMaximumFailedLoginInterval = 900; SOGoMaximumFailedLoginInterval = 900;
SOGoFailedLoginBlockInterval = 900; SOGoFailedLoginBlockInterval = 900;
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
//SOGoURLEncryptionEnabled = NO;
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
GCSChannelCollectionTimer = 60; GCSChannelCollectionTimer = 60;
GCSChannelExpireAge = 60; GCSChannelExpireAge = 60;
+27 -10
View File
@@ -49,6 +49,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Default to 1 yr // Default to 1 yr
$_data["validity"] = 8760; $_data["validity"] = 8760;
} }
if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
$permanent = 1;
}
else {
$permanent = 0;
}
$domain = $_data['domain']; $domain = $_data['domain'];
$description = $_data['description']; $description = $_data['description'];
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; $valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
@@ -65,13 +71,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false; return false;
} }
$validity = strtotime("+" . $_data["validity"] . " hour"); $validity = strtotime("+" . $_data["validity"] . " hour");
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES
(:address, :description, :goto, :validity)"); (:address, :description, :goto, :validity, :permanent)");
$stmt->execute(array( $stmt->execute(array(
':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain, ':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain,
':description' => $description, ':description' => $description,
':goto' => $username, ':goto' => $username,
':validity' => $validity ':validity' => $validity,
':permanent' => $permanent
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -2103,15 +2110,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
if (empty($_data['validity'])) { if (empty($_data['validity']) && empty($_data['permanent'])) {
continue; continue;
} }
$validity = round((int)time() + ($_data['validity'] * 3600)); if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) {
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE $permanent = 1;
$validity = 0;
}
else if (isset($_data['validity'])) {
$permanent = 0;
$validity = round((int)time() + ($_data['validity'] * 3600));
}
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE
`address` = :address"); `address` = :address");
$stmt->execute(array( $stmt->execute(array(
':address' => $address, ':address' => $address,
':validity' => $validity ':validity' => $validity,
':permanent' => $permanent
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -4584,10 +4599,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`description`, `description`,
`validity`, `validity`,
`created`, `created`,
`modified` `modified`,
`permanent`
FROM `spamalias` FROM `spamalias`
WHERE `goto` = :username WHERE `goto` = :username
AND `validity` >= :unixnow"); AND (`validity` >= :unixnow
OR `permanent` != 0)");
$stmt->execute(array(':username' => $_data, ':unixnow' => time())); $stmt->execute(array(':username' => $_data, ':unixnow' => time()));
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC); $tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $tladata; return $tladata;
@@ -5162,7 +5179,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username"); $stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username");
$stmt->execute(array(':domain' => $row['domain'], ':username' => $_data)); $stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
$MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC); $MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow"); $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)");
$stmt->execute(array(':address' => $_data, ':unixnow' => time())); $stmt->execute(array(':address' => $_data, ':unixnow' => time()));
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC); $SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use']; $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
+3 -2
View File
@@ -4,7 +4,7 @@ function init_db_schema()
try { try {
global $pdo; global $pdo;
$db_version = "07102025_1015"; $db_version = "10312025_0525";
$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));
@@ -554,7 +554,8 @@ function init_db_schema()
"description" => "TEXT NOT NULL", "description" => "TEXT NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"validity" => "INT(11)" "validity" => "INT(11)",
"permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
), ),
"keys" => array( "keys" => array(
"primary" => array( "primary" => array(
+1 -1
View File
@@ -85,7 +85,7 @@ $AVAILABLE_LANGUAGES = array(
// 'ca-es' => 'Català (Catalan)', // 'ca-es' => 'Català (Catalan)',
'bg-bg' => 'Български (Bulgarian)', 'bg-bg' => 'Български (Bulgarian)',
'cs-cz' => 'Čeština (Czech)', 'cs-cz' => 'Čeština (Czech)',
'da-dk' => 'Danish (Dansk)', 'da-dk' => 'Dansk (Danish)',
'de-de' => 'Deutsch (German)', 'de-de' => 'Deutsch (German)',
'en-gb' => 'English', 'en-gb' => 'English',
'es-es' => 'Español (Spanish)', 'es-es' => 'Español (Spanish)',
+19 -3
View File
@@ -175,6 +175,10 @@ jQuery(function($){
'</div>'; '</div>';
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />'; item.chkbox = '<input type="checkbox" class="form-check-input" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />';
item.address = escapeHtml(item.address); item.address = escapeHtml(item.address);
item.validity = {
value: item.validity,
permanent: item.permanent
};
} }
else { else {
item.chkbox = '<input type="checkbox" class="form-check-input" disabled />'; item.chkbox = '<input type="checkbox" class="form-check-input" disabled />';
@@ -218,9 +222,21 @@ jQuery(function($){
title: lang.alias_valid_until, title: lang.alias_valid_until,
data: 'validity', data: 'validity',
defaultContent: '', defaultContent: '',
createdCell: function(td, cellData) { render: function (data, type) {
createSortableDate(td, cellData) var date = new Date(data.value ? data.value * 1000 : 0);
} switch (type) {
case "sort":
if (data.permanent) {
return 0;
}
return date.getTime();
default:
if (data.permanent) {
return lang.forever;
}
return date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
}
},
}, },
{ {
title: lang.created_on, title: lang.created_on,
+5 -2
View File
@@ -987,7 +987,7 @@
"sogo_visible": "Alias Sichtbarkeit in SOGo", "sogo_visible": "Alias Sichtbarkeit in SOGo",
"sogo_visible_n": "Alias in SOGo verbergen", "sogo_visible_n": "Alias in SOGo verbergen",
"sogo_visible_y": "Alias in SOGo anzeigen", "sogo_visible_y": "Alias in SOGo anzeigen",
"spam_aliases": "Temp. Alias", "spam_aliases": "Spam-Alias",
"stats": "Statistik", "stats": "Statistik",
"status": "Status", "status": "Status",
"sync_jobs": "Synchronisationen", "sync_jobs": "Synchronisationen",
@@ -1281,7 +1281,9 @@
"encryption": "Verschlüsselung", "encryption": "Verschlüsselung",
"excludes": "Ausschlüsse", "excludes": "Ausschlüsse",
"expire_in": "Ungültig in", "expire_in": "Ungültig in",
"expire_never": "Niemals ungültig",
"fido2_webauthn": "FIDO2/WebAuthn", "fido2_webauthn": "FIDO2/WebAuthn",
"forever": "Für immer",
"force_pw_update": "Das Passwort für diesen Benutzer <b>muss</b> geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.", "force_pw_update": "Das Passwort für diesen Benutzer <b>muss</b> geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.",
"from": "von", "from": "von",
"generate": "generieren", "generate": "generieren",
@@ -1346,7 +1348,8 @@
"sogo_profile_reset": "SOGo-Profil zurücksetzen", "sogo_profile_reset": "SOGo-Profil zurücksetzen",
"sogo_profile_reset_help": "Das Profil wird inklusive <b>aller</b> Kalender- und Kontaktdaten <b>unwiederbringlich gelöscht</b>.", "sogo_profile_reset_help": "Das Profil wird inklusive <b>aller</b> Kalender- und Kontaktdaten <b>unwiederbringlich gelöscht</b>.",
"sogo_profile_reset_now": "Profil jetzt zurücksetzen", "sogo_profile_reset_now": "Profil jetzt zurücksetzen",
"spam_aliases": "Temporäre E-Mail-Aliasse", "spam_aliases": "Spam E-Mail-Aliasse",
"spam_aliases_info": "Ein Spam-Alias ist eine temporäre E-Mailadresse, die benutzt werden kann, um eine echte E-Mail Adressen zu schützen. <br>Optional kann eine Ablaufzeit gesetzt werden, sodass der Alias nach dem definierten Zeitraum automatisch deaktiviert wird, was missbrauchte oder geleakte Adressen effektiv entsorgt.",
"spam_score_reset": "Auf Server-Standard zurücksetzen", "spam_score_reset": "Auf Server-Standard zurücksetzen",
"spamfilter": "Spamfilter", "spamfilter": "Spamfilter",
"spamfilter_behavior": "Bewertung", "spamfilter_behavior": "Bewertung",
+4 -1
View File
@@ -1288,7 +1288,9 @@
"encryption": "Encryption", "encryption": "Encryption",
"excludes": "Excludes", "excludes": "Excludes",
"expire_in": "Expire in", "expire_in": "Expire in",
"expire_never": "Never Expire",
"fido2_webauthn": "FIDO2/WebAuthn", "fido2_webauthn": "FIDO2/WebAuthn",
"forever": "Forever",
"force_pw_update": "You <b>must</b> set a new password to be able to access groupware related services.", "force_pw_update": "You <b>must</b> set a new password to be able to access groupware related services.",
"from": "from", "from": "from",
"generate": "generate", "generate": "generate",
@@ -1355,7 +1357,8 @@
"sogo_profile_reset": "Reset SOGo profile", "sogo_profile_reset": "Reset SOGo profile",
"sogo_profile_reset_help": "This will destroy a user's SOGo profile and <b>delete all contact and calendar data irretrievable</b>.", "sogo_profile_reset_help": "This will destroy a user's SOGo profile and <b>delete all contact and calendar data irretrievable</b>.",
"sogo_profile_reset_now": "Reset profile now", "sogo_profile_reset_now": "Reset profile now",
"spam_aliases": "Temporary email aliases", "spam_aliases": "Spam email aliases",
"spam_aliases_info": "A spam alias is a temporary email address that can be used to protect real email addresses. <br>Optionally, an expiration time can be set so that the alias is automatically deactivated after the defined period, effectively disposing of abused or leaked addresses.",
"spam_score_reset": "Reset to server default", "spam_score_reset": "Reset to server default",
"spamfilter": "Spam filter", "spamfilter": "Spam filter",
"spamfilter_behavior": "Rating", "spamfilter_behavior": "Rating",
+6 -1
View File
@@ -1084,6 +1084,7 @@
"aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)", "aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)",
"change_password": "Cambiar contraseña", "change_password": "Cambiar contraseña",
"create_syncjob": "Crear nuevo trabajo de sincronización", "create_syncjob": "Crear nuevo trabajo de sincronización",
"created_on": "Creado",
"daily": "Cada día", "daily": "Cada día",
"day": "Día", "day": "Día",
"description": "Descripción", "description": "Descripción",
@@ -1095,6 +1096,9 @@
"edit": "Editar", "edit": "Editar",
"encryption": "Cifrado", "encryption": "Cifrado",
"excludes": "Excluye", "excludes": "Excluye",
"expire_in": "Expirará en",
"expire_never": "Nunca expirará",
"forever": "Siempre",
"hour": "Hora", "hour": "Hora",
"hourly": "Cada hora", "hourly": "Cada hora",
"hours": "Horas", "hours": "Horas",
@@ -1115,7 +1119,8 @@
"shared_aliases": "Alias compartidos", "shared_aliases": "Alias compartidos",
"shared_aliases_desc": "Los alias compartidos no se ven afectados por la configuración específica del usuario, como el filtro de correo no deseado o la política de cifrado. Los filtros de spam correspondientes solo pueden ser realizados por un administrador como una política de dominio.", "shared_aliases_desc": "Los alias compartidos no se ven afectados por la configuración específica del usuario, como el filtro de correo no deseado o la política de cifrado. Los filtros de spam correspondientes solo pueden ser realizados por un administrador como una política de dominio.",
"sogo_profile_reset": "Resetear perfil SOGo", "sogo_profile_reset": "Resetear perfil SOGo",
"spam_aliases": "Alias de email temporales", "spam_aliases": "Alias de email de spam",
"spam_aliases_info": "Un alias de spam es una dirección de correo electrónico temporal que se puede usar para proteger direcciones de correo electrónico reales. <br>Opcionalmente, se puede establecer un tiempo de expiración para que el alias se desactive automáticamente después del período definido, eliminando efectivamente las direcciones abusadas o filtradas.",
"spamfilter": "Filtro anti-spam", "spamfilter": "Filtro anti-spam",
"spamfilter_behavior": "Clasificación", "spamfilter_behavior": "Clasificación",
"spamfilter_bl": "Lista negra", "spamfilter_bl": "Lista negra",
+3
View File
@@ -1187,6 +1187,7 @@
"created_on": "作成日", "created_on": "作成日",
"daily": "毎日", "daily": "毎日",
"day": "日", "day": "日",
"description": "説明",
"delete_ays": "削除プロセスを確認してください。", "delete_ays": "削除プロセスを確認してください。",
"direct_aliases": "直接エイリアスアドレス", "direct_aliases": "直接エイリアスアドレス",
"direct_aliases_desc": "直接エイリアスアドレスは、スパムフィルターおよびTLSポリシー設定の影響を受けます。", "direct_aliases_desc": "直接エイリアスアドレスは、スパムフィルターおよびTLSポリシー設定の影響を受けます。",
@@ -1201,7 +1202,9 @@
"encryption": "暗号化", "encryption": "暗号化",
"excludes": "除外", "excludes": "除外",
"expire_in": "有効期限まで", "expire_in": "有効期限まで",
"expire_never": "有効期限なし",
"fido2_webauthn": "FIDO2/WebAuthn", "fido2_webauthn": "FIDO2/WebAuthn",
"forever": "有効期限なし",
"force_pw_update": "グループウェア関連サービスにアクセスするには、新しいパスワードを<b>必ず</b>設定する必要があります。", "force_pw_update": "グループウェア関連サービスにアクセスするには、新しいパスワードを<b>必ず</b>設定する必要があります。",
"from": "送信元", "from": "送信元",
"generate": "生成", "generate": "生成",
+3 -2
View File
@@ -185,11 +185,12 @@
"protocol_access": "Endre protokolltilgang", "protocol_access": "Endre protokolltilgang",
"pushover": "Pushover", "pushover": "Pushover",
"quarantine": "Karantenehandlinger", "quarantine": "Karantenehandlinger",
"quarantine_attachments": "Sett vedlegg i karantene", "quarantine_attachments": "Se vedlegg i karantene",
"quarantine_category": "Endre varslingskategori for karantene", "quarantine_category": "Endre varslingskategori for karantene",
"quarantine_notification": "Endre karantenevarslinger", "quarantine_notification": "Endre karantenevarslinger",
"domain_desc": "Endre domenebeskrivelse", "domain_desc": "Endre domenebeskrivelse",
"extend_sender_acl": "Tillat utvidelse av sender-ACL fra eksterne adresser" "extend_sender_acl": "Tillat utvidelse av sender-ACL fra eksterne adresser",
"pw_reset": "Tillat endring av brukerpassord"
}, },
"add": { "add": {
"app_passwd_protocols": "Tillatte protokoller for app-passord", "app_passwd_protocols": "Tillatte protokoller for app-passord",
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -691,6 +691,7 @@
"internal": "Nội bộ", "internal": "Nội bộ",
"internal_info": "Bí danh nội bộ chỉ có thể truy cập từ tên miền sở hữu hoặc tên miền bí danh.", "internal_info": "Bí danh nội bộ chỉ có thể truy cập từ tên miền sở hữu hoặc tên miền bí danh.",
"kind": "Loại", "kind": "Loại",
"last_modified": "Sửa đổi lần cuối" "last_modified": "Sửa đổi lần cuối",
"lookup_mx": "Đích là một biểu thức chính quy để khớp với tên MX (<code>.*.google.com</code> để định tuyến tất cả thư nhắm đến MX kết thúc bằng google.com qua bước nhảy này)"
} }
} }
+8 -6
View File
@@ -8,6 +8,7 @@
</div> </div>
<div id="collapse-tab-SpamAliases" class="card-body collapse" data-bs-parent="#user-content"> <div id="collapse-tab-SpamAliases" class="card-body collapse" data-bs-parent="#user-content">
<div class="row"> <div class="row">
<p>{{ lang.user.spam_aliases_info|raw }}</p>
<div class="col-md-12 col-sm-12 col-12"> <div class="col-md-12 col-sm-12 col-12">
<table id="tla_table" class="table table-striped dt-responsive w-100"></table> <table id="tla_table" class="table table-striped dt-responsive w-100"></table>
</div> </div>
@@ -18,12 +19,13 @@
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="tla" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a> <a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="tla" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a> <a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"1"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.hour }}</a></li> <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"1","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.hour }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"24"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.day }}</a></li> <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"24","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.day }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"168"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.week }}</a></li> <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"168","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.week }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"744"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.month }}</a></li> <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"744","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.month }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"8760"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.year }}</a></li> <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"8760","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.year }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"87600"}' href="#">{{ lang.user.expire_in }} 10 {{ lang.user.years }}</a></li> <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"87600","permanent":"0"}' href="#">{{ lang.user.expire_in }} 10 {{ lang.user.years }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"permanent":"1"}' href="#">{{ lang.user.expire_never }}</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="tla" data-api-url='delete/time_limited_alias' href="#">{{ lang.mailbox.remove }}</a></li> <li><a class="dropdown-item" data-action="delete_selected" data-id="tla" data-api-url='delete/time_limited_alias' href="#">{{ lang.mailbox.remove }}</a></li>
</ul> </ul>
+19 -19
View File
@@ -117,7 +117,7 @@ services:
- rspamd - rspamd
php-fpm-mailcow: php-fpm-mailcow:
image: ghcr.io/mailcow/phpfpm:1.94 image: ghcr.io/mailcow/phpfpm:8.2.29
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
@@ -188,10 +188,10 @@ services:
restart: always restart: always
labels: labels:
ofelia.enabled: "true" ofelia.enabled: "true"
ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m" ofelia.job-exec.phpfpm_keycloak_sync.schedule: "0 * * * * *"
ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true" ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true"
ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /crons/keycloak-sync.php || exit 0\"" ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /crons/keycloak-sync.php || exit 0\""
ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m" ofelia.job-exec.phpfpm_ldap_sync.schedule: "0 * * * * *"
ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true" ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true"
ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\"" ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\""
networks: networks:
@@ -200,7 +200,7 @@ services:
- phpfpm - phpfpm
sogo-mailcow: sogo-mailcow:
image: ghcr.io/mailcow/sogo:1.136 image: ghcr.io/mailcow/sogo:5.12.4
environment: environment:
- DBNAME=${DBNAME} - DBNAME=${DBNAME}
- DBUSER=${DBUSER} - DBUSER=${DBUSER}
@@ -236,13 +236,13 @@ services:
- sogo-userdata-backup-vol-1:/sogo_backup - sogo-userdata-backup-vol-1:/sogo_backup
labels: labels:
ofelia.enabled: "true" ofelia.enabled: "true"
ofelia.job-exec.sogo_sessions.schedule: "@every 1m" ofelia.job-exec.sogo_sessions.schedule: "0 * * * * *"
ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\"" ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\""
ofelia.job-exec.sogo_ealarms.schedule: "@every 1m" ofelia.job-exec.sogo_ealarms.schedule: "0 * * * * *"
ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/cron.creds || exit 0\"" ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/cron.creds || exit 0\""
ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m" ofelia.job-exec.sogo_eautoreply.schedule: "0 */5 * * * *"
ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/cron.creds || exit 0\"" ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds || exit 0\""
ofelia.job-exec.sogo_backup.schedule: "@every 24h" ofelia.job-exec.sogo_backup.schedule: "0 0 0 * * *"
ofelia.job-exec.sogo_backup.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool backup /sogo_backup ALL || exit 0\"" ofelia.job-exec.sogo_backup.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool backup /sogo_backup ALL || exit 0\""
restart: always restart: always
networks: networks:
@@ -252,7 +252,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: ghcr.io/mailcow/dovecot:2.35 image: ghcr.io/mailcow/dovecot:2.3.21.1
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
- netfilter-mailcow - netfilter-mailcow
@@ -310,22 +310,22 @@ services:
tty: true tty: true
labels: labels:
ofelia.enabled: "true" ofelia.enabled: "true"
ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m" ofelia.job-exec.dovecot_imapsync_runner.schedule: "0 * * * * *"
ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true" ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true"
ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"" ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\""
ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m" ofelia.job-exec.dovecot_trim_logs.schedule: "0 * * * * *"
ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"" ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\""
ofelia.job-exec.dovecot_quarantine.schedule: "@every 20m" ofelia.job-exec.dovecot_quarantine.schedule: "0 */20 * * * *"
ofelia.job-exec.dovecot_quarantine.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/quarantine_notify.py || exit 0\"" ofelia.job-exec.dovecot_quarantine.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/quarantine_notify.py || exit 0\""
ofelia.job-exec.dovecot_clean_q_aged.schedule: "@every 24h" ofelia.job-exec.dovecot_clean_q_aged.schedule: "0 0 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_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: "@every 30m" 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: "@every 24h" ofelia.job-exec.dovecot_sarules.schedule: "0 0 0 * * *"
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: "@every 24h" 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\""
ofelia.job-exec.dovecot_repl_health.schedule: "@every 5m" ofelia.job-exec.dovecot_repl_health.schedule: "0 */5 * * * *"
ofelia.job-exec.dovecot_repl_health.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/repl_health.sh\"" ofelia.job-exec.dovecot_repl_health.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/repl_health.sh\""
ulimits: ulimits:
nproc: 65535 nproc: 65535
@@ -339,7 +339,7 @@ services:
- dovecot - dovecot
postfix-mailcow: postfix-mailcow:
image: ghcr.io/mailcow/postfix:1.81 image: ghcr.io/mailcow/postfix:3.7.11
depends_on: depends_on:
mysql-mailcow: mysql-mailcow:
condition: service_started condition: service_started
+88 -35
View File
@@ -110,32 +110,32 @@ function backup() {
docker run --name mailcow-backup --rm \ docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_vmail.tar.zst /vmail
;;& ;;&
crypt|all) crypt|all)
docker run --name mailcow-backup --rm \ docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_crypt.tar.zst /crypt
;;& ;;&
redis|all) redis|all)
docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save
docker run --name mailcow-backup --rm \ docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_redis.tar.zst /redis
;;& ;;&
rspamd|all) rspamd|all)
docker run --name mailcow-backup --rm \ docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_rspamd.tar.zst /rspamd
;;& ;;&
postfix|all) postfix|all)
docker run --name mailcow-backup --rm \ docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_postfix.tar.zst /postfix
;;& ;;&
mysql|all) mysql|all)
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
@@ -154,7 +154,7 @@ function backup() {
${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \ ${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \
mariabackup --prepare --target-dir=/backup_mariadb ; \ mariabackup --prepare --target-dir=/backup_mariadb ; \
chown -R 999:999 /backup_mariadb ; \ chown -R 999:999 /backup_mariadb ; \
/bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;" /bin/tar --warning='no-file-ignored' --use-compress-program='zstd --rsyncable' -Pcvpf /backup/backup_mariadb.tar.zst /backup_mariadb ;"
fi fi
;;& ;;&
--delete-days) --delete-days)
@@ -170,6 +170,19 @@ function backup() {
done done
} }
function get_archive_info() {
local backup_name="$1"
local location="$2"
if [[ -f "${location}/${backup_name}.tar.zst" ]]; then
echo "${backup_name}.tar.zst|zstd -d -T${THREADS}"
elif [[ -f "${location}/${backup_name}.tar.gz" ]]; then
echo "${backup_name}.tar.gz|pigz -d -p ${THREADS}"
else
echo ""
fi
}
function restore() { function restore() {
for bin in docker; do for bin in docker; do
if [[ -z $(which ${bin}) ]]; then if [[ -z $(which ${bin}) ]]; then
@@ -199,10 +212,17 @@ function restore() {
case "$1" in case "$1" in
vmail) vmail)
docker stop $(docker ps -qf name=dovecot-mailcow) docker stop $(docker ps -qf name=dovecot-mailcow)
docker run -i --name mailcow-backup --rm \ ARCHIVE_INFO=$(get_archive_info "backup_vmail" "${RESTORE_LOCATION}")
-v ${RESTORE_LOCATION}:/backup:z \ if [[ -z "${ARCHIVE_INFO}" ]]; then
-v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \ echo -e "\e[31mError: No backup file found for vmail (searched for .tar.zst and .tar.gz)\e[0m"
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=dovecot-mailcow) docker start $(docker ps -aqf name=dovecot-mailcow)
echo echo
echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:"
@@ -218,31 +238,50 @@ function restore() {
;; ;;
redis) redis)
docker stop $(docker ps -qf name=redis-mailcow) docker stop $(docker ps -qf name=redis-mailcow)
docker run -i --name mailcow-backup --rm \ ARCHIVE_INFO=$(get_archive_info "backup_redis" "${RESTORE_LOCATION}")
-v ${RESTORE_LOCATION}:/backup:z \ if [[ -z "${ARCHIVE_INFO}" ]]; then
-v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \ echo -e "\e[31mError: No backup file found for redis (searched for .tar.zst and .tar.gz)\e[0m"
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=redis-mailcow) docker start $(docker ps -aqf name=redis-mailcow)
;; ;;
crypt) crypt)
docker stop $(docker ps -qf name=dovecot-mailcow) docker stop $(docker ps -qf name=dovecot-mailcow)
docker run -i --name mailcow-backup --rm \ ARCHIVE_INFO=$(get_archive_info "backup_crypt" "${RESTORE_LOCATION}")
-v ${RESTORE_LOCATION}:/backup:z \ if [[ -z "${ARCHIVE_INFO}" ]]; then
-v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \ echo -e "\e[31mError: No backup file found for crypt (searched for .tar.zst and .tar.gz)\e[0m"
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=dovecot-mailcow) docker start $(docker ps -aqf name=dovecot-mailcow)
;; ;;
rspamd) rspamd)
if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then ARCHIVE_INFO=$(get_archive_info "backup_rspamd" "${RESTORE_LOCATION}")
if [[ -z "${ARCHIVE_INFO}" ]]; then
echo -e "\e[31mError: No backup file found for rspamd (searched for .tar.zst and .tar.gz)\e[0m"
elif [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then
echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?"
sleep 2 sleep 2
echo -e "Continuing anyhow. If rspamd is crashing upon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" echo -e "Continuing anyhow. If rspamd is crashing upon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m"
sleep 2 sleep 2
docker stop $(docker ps -qf name=rspamd-mailcow) docker stop $(docker ps -qf name=rspamd-mailcow)
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
docker run -i --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
docker start $(docker ps -aqf name=rspamd-mailcow) docker start $(docker ps -aqf name=rspamd-mailcow)
elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then
echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..."
@@ -250,19 +289,28 @@ function restore() {
echo -e "Skipping rspamd due to compatibility issues!\e[0m" echo -e "Skipping rspamd due to compatibility issues!\e[0m"
else else
docker stop $(docker ps -qf name=rspamd-mailcow) docker stop $(docker ps -qf name=rspamd-mailcow)
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
docker run -i --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
docker start $(docker ps -aqf name=rspamd-mailcow) docker start $(docker ps -aqf name=rspamd-mailcow)
fi fi
;; ;;
postfix) postfix)
docker stop $(docker ps -qf name=postfix-mailcow) docker stop $(docker ps -qf name=postfix-mailcow)
docker run -i --name mailcow-backup --rm \ ARCHIVE_INFO=$(get_archive_info "backup_postfix" "${RESTORE_LOCATION}")
-v ${RESTORE_LOCATION}:/backup:z \ if [[ -z "${ARCHIVE_INFO}" ]]; then
-v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \ echo -e "\e[31mError: No backup file found for postfix (searched for .tar.zst and .tar.gz)\e[0m"
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=postfix-mailcow) docker start $(docker ps -aqf name=postfix-mailcow)
;; ;;
mysql|mariadb) mysql|mariadb)
@@ -305,14 +353,19 @@ function restore() {
echo Restoring... && \ echo Restoring... && \
gunzip < backup/backup_mysql.gz | mysql -uroot && \ gunzip < backup/backup_mysql.gz | mysql -uroot && \
mysql -uroot -e SHUTDOWN;" mysql -uroot -e SHUTDOWN;"
elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then else
docker run --name mailcow-backup --rm \ ARCHIVE_INFO=$(get_archive_info "backup_mariadb" "${RESTORE_LOCATION}")
-v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ if [[ -n "${ARCHIVE_INFO}" ]]; then
--entrypoint= \ ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
-v ${RESTORE_LOCATION}:/backup:z \ DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ docker run --name mailcow-backup --rm \
/bin/rm -rf /backup_mariadb/* ; \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \
/bin/tar -Pxvzf /backup/backup_mariadb.tar.gz" --entrypoint= \
-v ${RESTORE_LOCATION}:/backup:z \
${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \
/bin/rm -rf /backup_mariadb/* ; \
/bin/tar --use-compress-program='${DECOMPRESS_PROG}' -Pxvf /backup/${ARCHIVE_FILE}"
fi
fi fi
echo "Modifying mailcow.conf..." echo "Modifying mailcow.conf..."
source ${RESTORE_LOCATION}/mailcow.conf source ${RESTORE_LOCATION}/mailcow.conf
@@ -363,8 +416,8 @@ elif [[ ${1} == "restore" ]]; then
fi fi
echo "[ 0 ] - all" echo "[ 0 ] - all"
# find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz # find all files in folder with *.zst or *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .zst/.gz
FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.zst' -o -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//' | sort -u)
for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do
if [[ ${file} =~ vmail ]]; then if [[ ${file} =~ vmail ]]; then
echo "[ ${i} ] - Mail directory (/var/vmail)" echo "[ ${i} ] - Mail directory (/var/vmail)"
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env bash
# Test script for backup_and_restore.sh
# Tests backward compatibility with .tar.gz and new .tar.zst format
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BACKUP_IMAGE="${BACKUP_IMAGE:-ghcr.io/mailcow/backup:latest}"
TEST_DIR="/tmp/mailcow_backup_test_$$"
THREADS=2
echo "=== Mailcow Backup & Restore Test Suite ==="
echo "Test directory: ${TEST_DIR}"
echo "Backup image: ${BACKUP_IMAGE}"
echo ""
# Cleanup function
cleanup() {
echo "Cleaning up test files..."
rm -rf "${TEST_DIR}"
docker rmi mailcow-backup-test 2>/dev/null || true
}
trap cleanup EXIT
# Create test directory structure
mkdir -p "${TEST_DIR}"/{test_data,backup_zst,backup_gz,restore_zst,restore_gz,backup_large_zst,backup_large_gz}
echo "Test data for mailcow backup compatibility test" > "${TEST_DIR}/test_data/test.txt"
echo "Additional file to verify complete restore" > "${TEST_DIR}/test_data/test2.txt"
# Build test backup image with zstd support
echo "=== Building backup image with zstd support ==="
docker build -t mailcow-backup-test "${SCRIPT_DIR}/../data/Dockerfiles/backup/" || {
echo "ERROR: Failed to build backup image"
exit 1
}
# Test 1: Create .tar.zst backup
echo ""
echo "=== Test 1: Creating .tar.zst backup ==="
docker run --rm \
-w /data \
-v "${TEST_DIR}/test_data:/data:ro" \
-v "${TEST_DIR}/backup_zst:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}" \
-cvpf /backup/backup_test.tar.zst . \
> /dev/null
echo "✓ .tar.zst backup created: $(ls -lh ${TEST_DIR}/backup_zst/backup_test.tar.zst | awk '{print $5}')"
# Test 2: Create .tar.gz backup
echo ""
echo "=== Test 2: Creating .tar.gz backup (legacy) ==="
docker run --rm \
-w /data \
-v "${TEST_DIR}/test_data:/data:ro" \
-v "${TEST_DIR}/backup_gz:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="pigz --rsyncable -p ${THREADS}" \
-cvpf /backup/backup_test.tar.gz . \
> /dev/null
echo "✓ .tar.gz backup created: $(ls -lh ${TEST_DIR}/backup_gz/backup_test.tar.gz | awk '{print $5}')"
# Test 3: Test get_archive_info function
echo ""
echo "=== Test 3: Testing get_archive_info function ==="
# Extract and test the function directly
get_archive_info() {
local backup_name="$1"
local location="$2"
if [[ -f "${location}/${backup_name}.tar.zst" ]]; then
echo "${backup_name}.tar.zst|zstd -d -T${THREADS}"
elif [[ -f "${location}/${backup_name}.tar.gz" ]]; then
echo "${backup_name}.tar.gz|pigz -d -p ${THREADS}"
else
echo ""
fi
}
# Test with .tar.zst
result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_zst")
if [[ "${result}" =~ "zstd" ]]; then
echo "✓ Correctly detects .tar.zst and returns zstd decompressor"
else
echo "✗ Failed to detect .tar.zst"
exit 1
fi
# Test with .tar.gz
result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_gz")
if [[ "${result}" =~ "pigz" ]]; then
echo "✓ Correctly detects .tar.gz and returns pigz decompressor"
else
echo "✗ Failed to detect .tar.gz"
exit 1
fi
# Test with no file
result=$(get_archive_info "backup_test" "${TEST_DIR}")
if [[ -z "${result}" ]]; then
echo "✓ Correctly returns empty when no backup file found"
else
echo "✗ Should return empty but got: ${result}"
exit 1
fi
# Test 4: Restore from .tar.zst
echo ""
echo "=== Test 4: Restoring from .tar.zst ==="
docker run --rm \
-w /restore \
-v "${TEST_DIR}/backup_zst:/backup:ro" \
-v "${TEST_DIR}/restore_zst:/restore" \
mailcow-backup-test \
/bin/tar --use-compress-program="zstd -d -T${THREADS}" -xvpf /backup/backup_test.tar.zst \
> /dev/null 2>&1
if [[ -f "${TEST_DIR}/restore_zst/test.txt" ]] && \
[[ -f "${TEST_DIR}/restore_zst/test2.txt" ]]; then
echo "✓ Successfully restored from .tar.zst"
else
echo "✗ Failed to restore from .tar.zst"
ls -la "${TEST_DIR}/restore_zst/" || true
exit 1
fi
# Test 5: Restore from .tar.gz
echo ""
echo "=== Test 5: Restoring from .tar.gz (backward compatibility) ==="
docker run --rm \
-w /restore \
-v "${TEST_DIR}/backup_gz:/backup:ro" \
-v "${TEST_DIR}/restore_gz:/restore" \
mailcow-backup-test \
/bin/tar --use-compress-program="pigz -d -p ${THREADS}" -xvpf /backup/backup_test.tar.gz \
> /dev/null 2>&1
if [[ -f "${TEST_DIR}/restore_gz/test.txt" ]] && \
[[ -f "${TEST_DIR}/restore_gz/test2.txt" ]]; then
echo "✓ Successfully restored from .tar.gz (backward compatible)"
else
echo "✗ Failed to restore from .tar.gz"
ls -la "${TEST_DIR}/restore_gz/" || true
exit 1
fi
# Test 6: Verify content integrity
echo ""
echo "=== Test 6: Verifying content integrity ==="
original_content=$(cat "${TEST_DIR}/test_data/test.txt")
zst_content=$(cat "${TEST_DIR}/restore_zst/test.txt")
gz_content=$(cat "${TEST_DIR}/restore_gz/test.txt")
if [[ "${original_content}" == "${zst_content}" ]] && \
[[ "${original_content}" == "${gz_content}" ]]; then
echo "✓ Content integrity verified for both formats"
else
echo "✗ Content mismatch detected"
exit 1
fi
# Test 7: Compare compression ratios
echo ""
echo "=== Test 7: Compression comparison ==="
zst_size=$(stat -f%z "${TEST_DIR}/backup_zst/backup_test.tar.zst" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_zst/backup_test.tar.zst")
gz_size=$(stat -f%z "${TEST_DIR}/backup_gz/backup_test.tar.gz" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_gz/backup_test.tar.gz")
improvement=$(echo "scale=2; (${gz_size} - ${zst_size}) * 100 / ${gz_size}" | bc)
echo " Small files - .tar.gz size: ${gz_size} bytes"
echo " Small files - .tar.zst size: ${zst_size} bytes"
echo " Small files - Improvement: ${improvement}% smaller with zstd"
# Test 8: Error handling - missing backup file
echo ""
echo "=== Test 8: Error handling - Missing backup file ==="
result=$(get_archive_info "nonexistent_backup" "${TEST_DIR}/backup_zst")
if [[ -z "${result}" ]]; then
echo "✓ Correctly handles missing backup files"
else
echo "✗ Should return empty for missing files"
exit 1
fi
# Test 9: Error handling - Empty directory
echo ""
echo "=== Test 9: Error handling - Empty directory ==="
mkdir -p "${TEST_DIR}/empty_dir"
result=$(get_archive_info "backup_test" "${TEST_DIR}/empty_dir")
if [[ -z "${result}" ]]; then
echo "✓ Correctly handles empty directories"
else
echo "✗ Should return empty for empty directories"
exit 1
fi
# Test 10: Priority test - .tar.zst preferred over .tar.gz
echo ""
echo "=== Test 10: Format priority - .tar.zst preferred ==="
mkdir -p "${TEST_DIR}/both_formats"
touch "${TEST_DIR}/both_formats/backup_test.tar.gz"
touch "${TEST_DIR}/both_formats/backup_test.tar.zst"
result=$(get_archive_info "backup_test" "${TEST_DIR}/both_formats")
if [[ "${result}" =~ "zstd" ]]; then
echo "✓ Correctly prefers .tar.zst when both formats exist"
else
echo "✗ Should prefer .tar.zst over .tar.gz"
exit 1
fi
# Test 11: Large file compression test
echo ""
echo "=== Test 11: Large file compression test ==="
mkdir -p "${TEST_DIR}/large_data"
# Create ~10MB of compressible data (log-like content)
for i in {1..50000}; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: Processing email message $i from user@example.com to recipient@domain.com" >> "${TEST_DIR}/large_data/maillog.txt"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: SMTP connection established from 192.168.1.$((i % 255))" >> "${TEST_DIR}/large_data/maillog.txt"
done 2>/dev/null
# Get size (portable: works on Linux and macOS)
if du --version 2>/dev/null | grep -q GNU; then
original_size=$(du -sb "${TEST_DIR}/large_data" | cut -f1)
else
# macOS
original_size=$(find "${TEST_DIR}/large_data" -type f -exec stat -f%z {} \; | awk '{sum+=$1} END {print sum}')
fi
echo " Original data size: $(echo "scale=2; ${original_size} / 1024 / 1024" | bc) MB"
# Backup with zstd
docker run --rm \
-w /data \
-v "${TEST_DIR}/large_data:/data:ro" \
-v "${TEST_DIR}/backup_large_zst:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}" \
-cvpf /backup/backup_large.tar.zst . \
> /dev/null 2>&1
# Backup with pigz
docker run --rm \
-w /data \
-v "${TEST_DIR}/large_data:/data:ro" \
-v "${TEST_DIR}/backup_large_gz:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="pigz --rsyncable -p ${THREADS}" \
-cvpf /backup/backup_large.tar.gz . \
> /dev/null 2>&1
zst_large_size=$(stat -f%z "${TEST_DIR}/backup_large_zst/backup_large.tar.zst" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_large_zst/backup_large.tar.zst" 2>/dev/null || echo "0")
gz_large_size=$(stat -f%z "${TEST_DIR}/backup_large_gz/backup_large.tar.gz" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_large_gz/backup_large.tar.gz" 2>/dev/null || echo "0")
if [[ ${zst_large_size} -gt 0 ]] && [[ ${gz_large_size} -gt 0 ]]; then
large_improvement=$(echo "scale=2; (${gz_large_size} - ${zst_large_size}) * 100 / ${gz_large_size}" | bc)
echo " .tar.gz compressed: $(echo "scale=2; ${gz_large_size} / 1024 / 1024" | bc) MB"
echo " .tar.zst compressed: $(echo "scale=2; ${zst_large_size} / 1024 / 1024" | bc) MB"
echo " Improvement: ${large_improvement}% smaller with zstd"
else
echo " ✗ Failed to get file sizes"
exit 1
fi
if [[ $(echo "${large_improvement} > 0" | bc) -eq 1 ]]; then
echo "✓ zstd provides better compression on realistic data"
else
echo "⚠ zstd compression similar or worse than gzip (unusual but not critical)"
fi
# Test 12: Thread scaling test
echo ""
echo "=== Test 12: Multi-threading verification ==="
# This test verifies that different thread counts work (not measuring speed difference)
for thread_count in 1 4; do
THREADS=${thread_count}
result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_zst")
if [[ "${result}" =~ "-T${thread_count}" ]]; then
echo "✓ Thread count ${thread_count} correctly configured"
else
echo "✗ Thread count not properly applied"
exit 1
fi
done
echo ""
echo "=== All tests passed! ==="
echo ""
echo "Summary:"
echo " ✓ zstd compression working"
echo " ✓ pigz compression working (legacy)"
echo " ✓ zstd decompression working"
echo " ✓ pigz decompression working (backward compatible)"
echo " ✓ Archive detection working"
echo " ✓ Content integrity verified"
echo " ✓ Format priority correct (.tar.zst preferred)"
echo " ✓ Error handling for missing files"
echo " ✓ Error handling for empty directories"
echo " ✓ Multi-threading configuration verified"
echo " ✓ Large file compression: ${large_improvement}% improvement"
echo " ✓ Small file compression: ${improvement}% improvement"