1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-06-14 02:20:28 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Peter 6e8a0c24e7 Update to trixie 2025-11-09 23:13:38 +01:00
22 changed files with 110 additions and 635 deletions
+3 -7
View File
@@ -1,11 +1,11 @@
# Contribution Guidelines # Contribution Guidelines
**_Last modified on 12th November 2025_** **_Last modified on 15th August 2024_**
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 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. **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.
## Topics ## Topics
@@ -27,18 +27,14 @@ 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 12th November 2025_** **_Last modified on 15th August 2024_**
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).
+1 -1
View File
@@ -1,3 +1,3 @@
FROM debian:trixie-slim FROM debian:trixie-slim
RUN apt update && apt install pigz zstd -y --no-install-recommends RUN apt update && apt install pigz -y --no-install-recommends
@@ -204,17 +204,16 @@ 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_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} args = allow_real_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() AND permanent = 0; DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
END; END;
// //
DELIMITER ; DELIMITER ;
@@ -18,15 +18,11 @@ 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_SERVER="${REDIS_SLAVEOF_IP}" export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
export REDIS_PORT="${REDIS_SLAVEOF_PORT}"
else else
export REDIS_SERVER="redis" export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
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
@@ -41,13 +37,16 @@ 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
+2 -2
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() OR permanent != 0) AND validity >= UNIX_TIMESTAMP()
EOF EOF
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
else else
postfix -c /opt/postfix/conf start postfix -c /opt/postfix/conf start
sleep 126144000 sleep 126144000
fi fi
+4
View File
@@ -50,6 +50,10 @@ 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,7 +13,6 @@ 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,6 +14,7 @@ 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;
-6
View File
@@ -86,12 +86,6 @@
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;
+10 -27
View File
@@ -49,12 +49,6 @@ 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'];
@@ -71,14 +65,13 @@ 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`, `permanent`) VALUES $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES
(:address, :description, :goto, :validity, :permanent)"); (:address, :description, :goto, :validity)");
$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',
@@ -2110,23 +2103,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
if (empty($_data['validity']) && empty($_data['permanent'])) { if (empty($_data['validity'])) {
continue; continue;
} }
if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) { $validity = round((int)time() + ($_data['validity'] * 3600));
$permanent = 1; $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
$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',
@@ -4599,12 +4584,10 @@ 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;
@@ -5179,7 +5162,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 OR `permanent` != 0)"); $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow");
$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'];
+2 -3
View File
@@ -4,7 +4,7 @@ function init_db_schema()
try { try {
global $pdo; global $pdo;
$db_version = "10312025_0525"; $db_version = "07102025_1015";
$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,8 +554,7 @@ 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(
+3 -19
View File
@@ -175,10 +175,6 @@ 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 />';
@@ -222,21 +218,9 @@ jQuery(function($){
title: lang.alias_valid_until, title: lang.alias_valid_until,
data: 'validity', data: 'validity',
defaultContent: '', defaultContent: '',
render: function (data, type) { createdCell: function(td, cellData) {
var date = new Date(data.value ? data.value * 1000 : 0); createSortableDate(td, cellData)
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,
+2 -5
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": "Spam-Alias", "spam_aliases": "Temp. Alias",
"stats": "Statistik", "stats": "Statistik",
"status": "Status", "status": "Status",
"sync_jobs": "Synchronisationen", "sync_jobs": "Synchronisationen",
@@ -1281,9 +1281,7 @@
"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",
@@ -1348,8 +1346,7 @@
"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": "Spam E-Mail-Aliasse", "spam_aliases": "Temporäre 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",
+1 -4
View File
@@ -1288,9 +1288,7 @@
"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",
@@ -1357,8 +1355,7 @@
"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": "Spam email aliases", "spam_aliases": "Temporary 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",
+1 -6
View File
@@ -1084,7 +1084,6 @@
"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",
@@ -1096,9 +1095,6 @@
"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",
@@ -1119,8 +1115,7 @@
"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 de spam", "spam_aliases": "Alias de email temporales",
"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,7 +1187,6 @@
"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ポリシー設定の影響を受けます。",
@@ -1202,9 +1201,7 @@
"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": "生成",
+7 -120
View File
@@ -646,35 +646,7 @@
"internal": "Wewnętrzny", "internal": "Wewnętrzny",
"internal_info": "Aliasów wewnętrznych można używać tylko w obrębie własnej domeny lub domen aliasów.", "internal_info": "Aliasów wewnętrznych można używać tylko w obrębie własnej domeny lub domen aliasów.",
"last_modified": "Ostatnio modyfikowany", "last_modified": "Ostatnio modyfikowany",
"lookup_mx": "Destination to wyrażenie regularne dopasowujące nazwę serwera MX (np. <code>.*.google.com</code> — aby kierować całą pocztę wysyłaną do MX kończących się na google.com przez ten hop).", "lookup_mx": "Destination to wyrażenie regularne dopasowujące nazwę serwera MX (np. <code>.*.google.com</code> — aby kierować całą pocztę wysyłaną do MX kończących się na google.com przez ten hop)."
"pushover_title": "Tytuł powiadomienia",
"pushover_sound": "Dźwięk powiadomienia",
"pushover_vars": "Gdy nie zdefiniowano żadnego filtra nadawcy, brane będą pod uwagę wszystkie wiadomości. Filtry oparte na wyrażeniach regularnych (regex) oraz dokładne dopasowania nadawców można definiować indywidualnie — będą one przetwarzane kolejno, niezależnie od siebie. Dostępne zmienne do użycia w treści i tytule (należy pamiętać o zasadach ochrony danych osobowych).",
"pushover_verify": "Zweryfikuj dane logowania",
"quota_warning_bcc": "Ukryta kopia ostrzeżenia o przekroczeniu limitu",
"quota_warning_bcc_info": "Ostrzeżenia będą wysyłane jako osobne kopie do poniższych odbiorców. Temat wiadomości zostanie rozszerzony o nazwę użytkownika w nawiasach, na przykład: <code>Ostrzeżenie o limicie (user@example.com\n)</code>.",
"ratelimit": "Limit wysyłania",
"redirect_uri": "Adres przekierowania / adres zwrotny (Redirect/Callback URL)",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Informacja</div> **Możesz zdefiniować mapy transportu (transport maps) dla niestandardowego miejsca docelowego tej domeny.** Jeśli nie zostanie to ustawione, zostanie wykonane wyszukiwanie",
"relay_unknown_only": "Przekazuj (relay) tylko nieistniejące skrzynki pocztowe. Istniejące skrzynki będą dostarczane lokalnie.",
"relayhost": "Transporty przypisane do nadawcy",
"scope": "Zakres",
"sender_acl_disabled": "<span class=\"badge fs-6 bg-danger\">Sprawdzanie nadawcy jest wyłączone</span>",
"sender_acl_info": "Jeżeli użytkownik skrzynki pocztowej A ma pozwolenie na wysyłkę jako użytkownik skrzynki B, adres nadawcy nie jest automatycznie wyświetlany jako opcja w polu „Od” w SOGo.<br>\nUżytkownik skrzynki B musi utworzyć delegację w SOGo, aby użytkownik A mógł wybrać ich adres jako nadawcę. Aby zdelegować skrzynkę w SOGo, użyj menu (trzy kropki) po prawej stronie nazwy skrzynki w lewym górnym rogu, będąc w widoku poczty. To zachowanie nie dotyczy adresów aliasów.",
"sieve_desc": "Krótki opis",
"sieve_type": "Typ filtra",
"skipcrossduplicates": "Pomijaj duplikaty wiadomości w różnych folderach (pierwsza napotkana wiadomość zostaje zachowana).",
"sogo_access": "Bezpośrednie przekazywanie do SOGo",
"sogo_access_info": "Po zalogowaniu użytkownik jest automatycznie przekierowywany do SOGo.",
"sogo_visible": "Alias jest widoczny w SOGO",
"sogo_visible_info": "Ta opcja dotyczy tylko obiektów, które mogą być wyświetlane w SOGo (aliasy współdzielone lub nie współdzielone, wskazujące przynajmniej na jedną lokalną skrzynkę).\nJeśli obiekt zostanie ukryty, alias nie będzie dostępny jako wybieralny nadawca w SOGo.",
"spam_alias": "Tworzenie lub zmiana ograniczonych czasowo adresów aliasów",
"spam_filter": "Filtr spam",
"spam_policy": "Dodaj lub usuń elementy z listy dozwolonych / zablokowanych",
"spam_score": "Ustaw własny poziom punktacji spamu",
"timeout1": "Limit czasu połączenia z serwerem zdalnym",
"timeout2": "Limit czasu połączenia z serwerem lokalnym",
"validate_save": "Zatwierdź i zapisz"
}, },
"footer": { "footer": {
"cancel": "Anuluj", "cancel": "Anuluj",
@@ -682,14 +654,7 @@
"delete_now": "Usuń teraz", "delete_now": "Usuń teraz",
"delete_these_items": "Proszę potwierdzić zmiany w poniższym identyfikatorze obiektu", "delete_these_items": "Proszę potwierdzić zmiany w poniższym identyfikatorze obiektu",
"loading": "Proszę czekać...", "loading": "Proszę czekać...",
"restart_now": "Uruchom ponownie teraz", "restart_now": "Uruchom ponownie teraz"
"hibp_check": "Sprawdź w stosunku do haveibeenpwned.com",
"hibp_nok": "Dopasowano! To potencjalnie niebezpieczne hasło!",
"hibp_ok": "Nie znaleziono żadnego dopasowania.",
"nothing_selected": "Nic wybranego",
"restart_container": "Zresetuj kontener",
"restart_container_info": "<b>Ważne:</b> Łagodne ponowne uruchomienie może zająć trochę czasu — proszę poczekać na jego zakończenie.",
"restarting_container": "Ponowne uruchomienie kontenera, może to zająć trochę czasu"
}, },
"header": { "header": {
"administration": "Administrowanie", "administration": "Administrowanie",
@@ -697,16 +662,10 @@
"mailcow_config": "Konfiguracja", "mailcow_config": "Konfiguracja",
"quarantine": "Kwarantanna", "quarantine": "Kwarantanna",
"restart_sogo": "Uruchom ponownie SOGo", "restart_sogo": "Uruchom ponownie SOGo",
"user_settings": "Ustawienia użytkownika", "user_settings": "Ustawienia użytkownika"
"apps": "Aplikacje",
"debug": "Informacja",
"mailcow_system": "System",
"restart_netfilter": "Uruchom ponownie netfilter"
}, },
"info": { "info": {
"no_action": "Żadne działanie nie ma zastosowania", "no_action": "Żadne działanie nie ma zastosowania"
"awaiting_tfa_confirmation": "Oczekiwanie na potwierdzenie TFA",
"session_expires": "Twoja sesja wygaśnie za około 15 sekund"
}, },
"login": { "login": {
"delayed": "Logowanie zostało opóźnione o %s sekund.", "delayed": "Logowanie zostało opóźnione o %s sekund.",
@@ -719,18 +678,7 @@
"login_domainadmintext": "Zaloguj się jako administrator domeny", "login_domainadmintext": "Zaloguj się jako administrator domeny",
"login_admintext": "Zaloguj się jako admin", "login_admintext": "Zaloguj się jako admin",
"other_logins": "lub zaloguj za pomocą", "other_logins": "lub zaloguj za pomocą",
"email": "Adres e-mail", "email": "Adres e-mail"
"back_to_mailcow": "Wróć do mailcow",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"invalid_pass_reset_token": "Token resetowania hasła jest nieprawidłowy lub wygasł.<br>Proszę poprosić o nowy link do resetowania hasła.",
"login_user": "Logowanie użytkownika",
"login_dadmin": "Logowanie administratora domeny",
"login_admin": "Login Administratora",
"mobileconfig_info": "Zaloguj się jako użytkownik skrzynki pocztowej, aby pobrać żądany profil połączenia Apple.",
"new_password": "Nowe hasło",
"new_password_confirm": "Potwierdź nowe hasło",
"reset_password": "Zresetuj hasło",
"request_reset_password": "Poproś o zmianę hasła"
}, },
"mailbox": { "mailbox": {
"action": "Działanie", "action": "Działanie",
@@ -790,48 +738,7 @@
"tls_enforce_out": "Uruchom TLS wychodzące", "tls_enforce_out": "Uruchom TLS wychodzące",
"toggle_all": "Zaznacz wszystkie", "toggle_all": "Zaznacz wszystkie",
"username": "Nazwa użytkownika", "username": "Nazwa użytkownika",
"weekly": "Co tydzień", "weekly": "Co tydzień"
"add_alias_expand": "Rozszerz alias na domeny alias",
"add_bcc_entry": "Dodaj mapę BCC",
"add_filter": "Dodaj filtr",
"recipient_map_old_info": "Mapa odbiorcy (pierwotne miejsce docelowe) musi być prawidłowym adresem e-mail lub nazwą domeny.",
"recipient_maps": "Mapy odbiorców",
"relay_unknown": "Przekazuj nieznane skrzynki pocztowe",
"running": "Uruchomione",
"sender": "Nadawca",
"set_postfilter": "Oznacz jako postfilter",
"set_prefilter": "Oznacz jako prefilter",
"sieve_info": "Możesz przechowywać wiele filtrów dla każdego użytkownika, jednak w danym momencie aktywny może być tylko jeden prefilter i jeden postfilter<br>\nKażdy filtr będzie przetwarzany w opisanej kolejności. Ani błędny skrypt, ani polecenie „keep;” nie zatrzymają przetwarzania kolejnych skryptów.\nZmiany w globalnych skryptach Sieve spowodują ponowne uruchomienie usługi Dovecot.<br><br>\nGlobalny prefiltr Sieve • Prefilter • Skrypty użytkownika • Postfilter • Globalny postfilter Sieve",
"sieve_preset_1": "Odrzuć pocztę z prawdopodobnymi niebezpiecznymi typami plików",
"sieve_preset_2": "Zawsze zaznaczaj e-mail konkretnego nadawcy jako odczytane",
"sieve_preset_3": "Odrzuć po cichu, zatrzymaj cały proces sieve",
"sieve_preset_4": "Plik do INBOX, pomiń dalszy proces przez filtry sieve",
"sieve_preset_5": "Automatyczna odpowiedź (urlopowa)",
"sieve_preset_6": "Odrzuć pocztę z odpowiedzią",
"sieve_preset_7": "Przekierowanie wiadomości z możliwością zachowania lub usunięcia kopii",
"sieve_preset_8": "Przekieruj wiadomość e-mail od określonego nadawcy, oznacz jako odczytaną i posortuj do podfoldera",
"sieve_preset_header": "Zbacz przykładowe ustawienia poniżej. Więcej szczegółów znajdziesz w <a href=\"https://en.wikipedia.org/wiki/Sieve_(mail_filtering_language)\" target=\"_blank\">Wikipedii</a>.",
"sogo_visible": "Alias jest widoczny w SOGo",
"sogo_visible_n": "Ukryj alias w SOGo",
"sogo_visible_y": "Pokaż alias w SOGo",
"stats": "Statystyki",
"status": "Status",
"syncjob_check_log": "Sprawdź log",
"syncjob_last_run_result": "Wynik ostatniego uruchomienia",
"syncjob_EX_OK": "Sukces",
"syncjob_EXIT_CONNECTION_FAILURE": "Problem z połączeniem",
"syncjob_EXIT_TLS_FAILURE": "Problem z szyfrowanym połączeniem",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Problem uwierzytelniania",
"syncjob_EXIT_OVERQUOTA": "Docelowa skrzynka pocztowa przekroczyła limit pojemności",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło",
"table_size": "Rozmiar tabeli",
"table_size_show_n": "Pokaż %s elementy",
"templates": "Szablony",
"template": "Szablon",
"tls_map_dest": "Miejsce docelowe",
"tls_map_dest_info": "Przykłady: example.org, .example.org, [mail.example.org]:25",
"tls_map_parameters": "Parametry"
}, },
"quarantine": { "quarantine": {
"action": "Działanie", "action": "Działanie",
@@ -842,12 +749,7 @@
"toggle_all": "Zaznacz wszystkie", "toggle_all": "Zaznacz wszystkie",
"confirm_delete": "Potwierdź usunięcie tego elementu.", "confirm_delete": "Potwierdź usunięcie tego elementu.",
"learn_spam_delete": "Zapamiętaj jako spam i usuwaj w przyszłości", "learn_spam_delete": "Zapamiętaj jako spam i usuwaj w przyszłości",
"quick_delete_link": "Otwórz szybki link do usuwania", "quick_delete_link": "Otwórz szybki link do usuwania"
"refresh": "Odśwież",
"rejected": "Odrzucony",
"release": "Zwolnij",
"release_body": "Dołączyliśmy Twoją wiadomość jako plik eml do tej wiadomości.",
"release_subject": "Potencjalnie szkodliwa pozycja kwarantanny %s"
}, },
"queue": { "queue": {
"queue_manager": "Menedżer kolejki", "queue_manager": "Menedżer kolejki",
@@ -1182,20 +1084,5 @@
"dns_records_status": "Aktualny stan", "dns_records_status": "Aktualny stan",
"dns_records_type": "Typ", "dns_records_type": "Typ",
"optional": "Ten rekord jest opcjonalny." "optional": "Ten rekord jest opcjonalny."
},
"fido2": {
"confirm": "Potwierdź",
"fido2_auth": "Logowanie za pomocą FIDO2",
"fido2_success": "Urządzenie pomyślnie zarejestrowane",
"fido2_validation_failed": "Walidacja nie powiodła się",
"fn": "Przyjazna nazwa FIDO",
"known_ids": "Znane Id",
"none": "Wyłączony",
"register_status": "Status rejestracji",
"rename": "Zmień nazwę",
"set_fido2": "Zarejestruj urządzenie FIDO2",
"set_fido2_touchid": "Zarejestruj Touch ID w Apple M1",
"set_fn": "Ustaw przyjazną nazwę w FIDO",
"start_fido2_validation": "Rozpocznij walidację FIDO2"
} }
} }
+6 -8
View File
@@ -8,7 +8,6 @@
</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>
@@ -19,13 +18,12 @@
<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","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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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='{"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='{"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:8.2.29 image: ghcr.io/mailcow/phpfpm:1.94
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: "0 * * * * *" ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m"
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: "0 * * * * *" ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m"
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:5.12.4 image: ghcr.io/mailcow/sogo:1.136
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: "0 * * * * *" ofelia.job-exec.sogo_sessions.schedule: "@every 1m"
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: "0 * * * * *" ofelia.job-exec.sogo_ealarms.schedule: "@every 1m"
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: "0 */5 * * * *" ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m"
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_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_backup.schedule: "0 0 0 * * *" ofelia.job-exec.sogo_backup.schedule: "@every 24h"
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.3.21.1 image: ghcr.io/mailcow/dovecot:2.35
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: "0 * * * * *" ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m"
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: "0 * * * * *" ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m"
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: "0 */20 * * * *" ofelia.job-exec.dovecot_quarantine.schedule: "@every 20m"
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: "0 0 0 * * *" ofelia.job-exec.dovecot_clean_q_aged.schedule: "@every 24h"
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: "@every 30m"
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: "@every 24h"
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: "0 */5 * * * *" ofelia.job-exec.dovecot_repl_health.schedule: "@every 5m"
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:3.7.11 image: ghcr.io/mailcow/postfix:1.81
depends_on: depends_on:
mysql-mailcow: mysql-mailcow:
condition: service_started condition: service_started
+35 -88
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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_vmail.tar.zst /vmail ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_crypt.tar.zst /crypt ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_redis.tar.zst /redis ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_rspamd.tar.zst /rspamd ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_postfix.tar.zst /postfix ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /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='zstd --rsyncable' -Pcvpf /backup/backup_mariadb.tar.zst /backup_mariadb ;" /bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;"
fi fi
;;& ;;&
--delete-days) --delete-days)
@@ -170,19 +170,6 @@ 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
@@ -212,17 +199,10 @@ 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)
ARCHIVE_INFO=$(get_archive_info "backup_vmail" "${RESTORE_LOCATION}") docker run -i --name mailcow-backup --rm \
if [[ -z "${ARCHIVE_INFO}" ]]; then -v ${RESTORE_LOCATION}:/backup:z \
echo -e "\e[31mError: No backup file found for vmail (searched for .tar.zst and .tar.gz)\e[0m" -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \
else ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz
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:"
@@ -238,50 +218,31 @@ function restore() {
;; ;;
redis) redis)
docker stop $(docker ps -qf name=redis-mailcow) docker stop $(docker ps -qf name=redis-mailcow)
ARCHIVE_INFO=$(get_archive_info "backup_redis" "${RESTORE_LOCATION}") docker run -i --name mailcow-backup --rm \
if [[ -z "${ARCHIVE_INFO}" ]]; then -v ${RESTORE_LOCATION}:/backup:z \
echo -e "\e[31mError: No backup file found for redis (searched for .tar.zst and .tar.gz)\e[0m" -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \
else ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz
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)
ARCHIVE_INFO=$(get_archive_info "backup_crypt" "${RESTORE_LOCATION}") docker run -i --name mailcow-backup --rm \
if [[ -z "${ARCHIVE_INFO}" ]]; then -v ${RESTORE_LOCATION}:/backup:z \
echo -e "\e[31mError: No backup file found for crypt (searched for .tar.zst and .tar.gz)\e[0m" -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \
else ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz
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)
ARCHIVE_INFO=$(get_archive_info "backup_rspamd" "${RESTORE_LOCATION}") if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
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..."
@@ -289,28 +250,19 @@ 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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
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)
ARCHIVE_INFO=$(get_archive_info "backup_postfix" "${RESTORE_LOCATION}") docker run -i --name mailcow-backup --rm \
if [[ -z "${ARCHIVE_INFO}" ]]; then -v ${RESTORE_LOCATION}:/backup:z \
echo -e "\e[31mError: No backup file found for postfix (searched for .tar.zst and .tar.gz)\e[0m" -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \
else ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz
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)
@@ -353,19 +305,14 @@ 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;"
else elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then
ARCHIVE_INFO=$(get_archive_info "backup_mariadb" "${RESTORE_LOCATION}") docker run --name mailcow-backup --rm \
if [[ -n "${ARCHIVE_INFO}" ]]; then -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) --entrypoint= \
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) -v ${RESTORE_LOCATION}:/backup:z \
docker run --name mailcow-backup --rm \ ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ /bin/rm -rf /backup_mariadb/* ; \
--entrypoint= \ /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz"
-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
@@ -416,8 +363,8 @@ elif [[ ${1} == "restore" ]]; then
fi fi
echo "[ 0 ] - all" echo "[ 0 ] - all"
# find all files in folder with *.zst or *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .zst/.gz # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz
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) 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/\.[^.]*$//')
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)"
@@ -1,301 +0,0 @@
#!/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"