mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-02-18 15:16:25 +00:00
Compare commits
11 Commits
backup-tri
...
dragoangel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
156cfbeb4d | ||
|
|
0413d26855 | ||
|
|
7b29c1f304 | ||
|
|
ae3ef391ee | ||
|
|
7313f996d3 | ||
|
|
62d16c9e56 | ||
|
|
674b41ce08 | ||
|
|
1b833be760 | ||
|
|
88adb1adf5 | ||
|
|
ec472f13cf | ||
|
|
2e1d98cc7c |
@@ -1,11 +1,11 @@
|
||||
# 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!
|
||||
|
||||
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
|
||||
|
||||
@@ -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.*
|
||||
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!
|
||||
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
|
||||
**_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:
|
||||
|
||||
### 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
|
||||
|
||||
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,3 +1,3 @@
|
||||
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
|
||||
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
|
||||
# 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
|
||||
# Autogenerated by mailcow
|
||||
passdb {
|
||||
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
|
||||
|
||||
# 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
|
||||
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
||||
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
||||
|
||||
@@ -167,7 +167,7 @@ DELIMITER //
|
||||
CREATE EVENT clean_spamalias
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
|
||||
@@ -18,11 +18,15 @@ done
|
||||
|
||||
# Do not attempt to write to slave
|
||||
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
|
||||
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
|
||||
export REDIS_SERVER="redis"
|
||||
export REDIS_PORT="6379"
|
||||
fi
|
||||
|
||||
export REDIS_CMDLINE="redis-cli -h ${REDIS_SERVER} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||
|
||||
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis..."
|
||||
sleep 2
|
||||
@@ -37,16 +41,13 @@ echo "Postfix OK"
|
||||
cat <<EOF > /etc/postfix-tlspol/config.yaml
|
||||
server:
|
||||
address: 0.0.0.0:8642
|
||||
|
||||
log-level: ${LOGLVL}
|
||||
|
||||
prefetch: true
|
||||
|
||||
cache-file: /var/lib/postfix-tlspol/cache.db
|
||||
|
||||
dns:
|
||||
# must support DNSSEC
|
||||
address: 127.0.0.11:53
|
||||
redis:
|
||||
address: ${REDIS_SERVER}:${REDIS_PORT}
|
||||
db: 2
|
||||
EOF
|
||||
|
||||
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||
|
||||
@@ -390,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT goto FROM spamalias
|
||||
WHERE address='%s'
|
||||
AND validity >= UNIX_TIMESTAMP()
|
||||
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
|
||||
EOF
|
||||
|
||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||
@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -50,10 +50,6 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
<string>${RAND_PASS}</string>
|
||||
<key>SOGoURLEncryptionEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>SOGoURLEncryptionPassphrase</key>
|
||||
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
|
||||
<key>OCSAdminURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||
<key>OCSCacheFolderURL</key>
|
||||
|
||||
@@ -13,6 +13,7 @@ events {
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_tokens off;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
|
||||
@@ -14,7 +14,6 @@ ssl_session_tickets off;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=15768000;";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
add_header X-Download-Options noopen;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
SOGoMaximumFailedLoginInterval = 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;
|
||||
GCSChannelExpireAge = 60;
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
// Default to 1 yr
|
||||
$_data["validity"] = 8760;
|
||||
}
|
||||
if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
|
||||
$permanent = 1;
|
||||
}
|
||||
else {
|
||||
$permanent = 0;
|
||||
}
|
||||
$domain = $_data['domain'];
|
||||
$description = $_data['description'];
|
||||
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
|
||||
@@ -65,13 +71,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
$validity = strtotime("+" . $_data["validity"] . " hour");
|
||||
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES
|
||||
(:address, :description, :goto, :validity)");
|
||||
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES
|
||||
(:address, :description, :goto, :validity, :permanent)");
|
||||
$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,
|
||||
':description' => $description,
|
||||
':goto' => $username,
|
||||
':validity' => $validity
|
||||
':validity' => $validity,
|
||||
':permanent' => $permanent
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -2103,15 +2110,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($_data['validity'])) {
|
||||
if (empty($_data['validity']) && empty($_data['permanent'])) {
|
||||
continue;
|
||||
}
|
||||
$validity = round((int)time() + ($_data['validity'] * 3600));
|
||||
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
|
||||
if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) {
|
||||
$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");
|
||||
$stmt->execute(array(
|
||||
':address' => $address,
|
||||
':validity' => $validity
|
||||
':validity' => $validity,
|
||||
':permanent' => $permanent
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -4584,10 +4599,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`description`,
|
||||
`validity`,
|
||||
`created`,
|
||||
`modified`
|
||||
`modified`,
|
||||
`permanent`
|
||||
FROM `spamalias`
|
||||
WHERE `goto` = :username
|
||||
AND `validity` >= :unixnow");
|
||||
AND (`validity` >= :unixnow
|
||||
OR `permanent` != 0)");
|
||||
$stmt->execute(array(':username' => $_data, ':unixnow' => time()));
|
||||
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
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->execute(array(':domain' => $row['domain'], ':username' => $_data));
|
||||
$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()));
|
||||
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
|
||||
|
||||
@@ -4,7 +4,7 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "07102025_1015";
|
||||
$db_version = "10312025_0525";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -554,7 +554,8 @@ function init_db_schema()
|
||||
"description" => "TEXT NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"validity" => "INT(11)"
|
||||
"validity" => "INT(11)",
|
||||
"permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
|
||||
@@ -175,6 +175,10 @@ jQuery(function($){
|
||||
'</div>';
|
||||
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.validity = {
|
||||
value: item.validity,
|
||||
permanent: item.permanent
|
||||
};
|
||||
}
|
||||
else {
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" disabled />';
|
||||
@@ -218,9 +222,21 @@ jQuery(function($){
|
||||
title: lang.alias_valid_until,
|
||||
data: 'validity',
|
||||
defaultContent: '',
|
||||
createdCell: function(td, cellData) {
|
||||
createSortableDate(td, cellData)
|
||||
}
|
||||
render: function (data, type) {
|
||||
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,
|
||||
|
||||
@@ -987,7 +987,7 @@
|
||||
"sogo_visible": "Alias Sichtbarkeit in SOGo",
|
||||
"sogo_visible_n": "Alias in SOGo verbergen",
|
||||
"sogo_visible_y": "Alias in SOGo anzeigen",
|
||||
"spam_aliases": "Temp. Alias",
|
||||
"spam_aliases": "Spam-Alias",
|
||||
"stats": "Statistik",
|
||||
"status": "Status",
|
||||
"sync_jobs": "Synchronisationen",
|
||||
@@ -1281,7 +1281,9 @@
|
||||
"encryption": "Verschlüsselung",
|
||||
"excludes": "Ausschlüsse",
|
||||
"expire_in": "Ungültig in",
|
||||
"expire_never": "Niemals ungültig",
|
||||
"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.",
|
||||
"from": "von",
|
||||
"generate": "generieren",
|
||||
@@ -1346,7 +1348,8 @@
|
||||
"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_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",
|
||||
"spamfilter": "Spamfilter",
|
||||
"spamfilter_behavior": "Bewertung",
|
||||
|
||||
@@ -1288,7 +1288,9 @@
|
||||
"encryption": "Encryption",
|
||||
"excludes": "Excludes",
|
||||
"expire_in": "Expire in",
|
||||
"expire_never": "Never Expire",
|
||||
"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.",
|
||||
"from": "from",
|
||||
"generate": "generate",
|
||||
@@ -1355,7 +1357,8 @@
|
||||
"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_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",
|
||||
"spamfilter": "Spam filter",
|
||||
"spamfilter_behavior": "Rating",
|
||||
|
||||
@@ -1084,6 +1084,7 @@
|
||||
"aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)",
|
||||
"change_password": "Cambiar contraseña",
|
||||
"create_syncjob": "Crear nuevo trabajo de sincronización",
|
||||
"created_on": "Creado",
|
||||
"daily": "Cada día",
|
||||
"day": "Día",
|
||||
"description": "Descripción",
|
||||
@@ -1095,6 +1096,9 @@
|
||||
"edit": "Editar",
|
||||
"encryption": "Cifrado",
|
||||
"excludes": "Excluye",
|
||||
"expire_in": "Expirará en",
|
||||
"expire_never": "Nunca expirará",
|
||||
"forever": "Siempre",
|
||||
"hour": "Hora",
|
||||
"hourly": "Cada hora",
|
||||
"hours": "Horas",
|
||||
@@ -1115,7 +1119,8 @@
|
||||
"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.",
|
||||
"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_behavior": "Clasificación",
|
||||
"spamfilter_bl": "Lista negra",
|
||||
|
||||
@@ -1187,6 +1187,7 @@
|
||||
"created_on": "作成日",
|
||||
"daily": "毎日",
|
||||
"day": "日",
|
||||
"description": "説明",
|
||||
"delete_ays": "削除プロセスを確認してください。",
|
||||
"direct_aliases": "直接エイリアスアドレス",
|
||||
"direct_aliases_desc": "直接エイリアスアドレスは、スパムフィルターおよびTLSポリシー設定の影響を受けます。",
|
||||
@@ -1201,7 +1202,9 @@
|
||||
"encryption": "暗号化",
|
||||
"excludes": "除外",
|
||||
"expire_in": "有効期限まで",
|
||||
"expire_never": "有効期限なし",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||
"forever": "有効期限なし",
|
||||
"force_pw_update": "グループウェア関連サービスにアクセスするには、新しいパスワードを<b>必ず</b>設定する必要があります。",
|
||||
"from": "送信元",
|
||||
"generate": "生成",
|
||||
|
||||
@@ -646,7 +646,35 @@
|
||||
"internal": "Wewnętrzny",
|
||||
"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",
|
||||
"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": {
|
||||
"cancel": "Anuluj",
|
||||
@@ -654,7 +682,14 @@
|
||||
"delete_now": "Usuń teraz",
|
||||
"delete_these_items": "Proszę potwierdzić zmiany w poniższym identyfikatorze obiektu",
|
||||
"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": {
|
||||
"administration": "Administrowanie",
|
||||
@@ -662,10 +697,16 @@
|
||||
"mailcow_config": "Konfiguracja",
|
||||
"quarantine": "Kwarantanna",
|
||||
"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": {
|
||||
"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": {
|
||||
"delayed": "Logowanie zostało opóźnione o %s sekund.",
|
||||
@@ -678,7 +719,18 @@
|
||||
"login_domainadmintext": "Zaloguj się jako administrator domeny",
|
||||
"login_admintext": "Zaloguj się jako admin",
|
||||
"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": {
|
||||
"action": "Działanie",
|
||||
@@ -738,7 +790,48 @@
|
||||
"tls_enforce_out": "Uruchom TLS wychodzące",
|
||||
"toggle_all": "Zaznacz wszystkie",
|
||||
"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": {
|
||||
"action": "Działanie",
|
||||
@@ -749,7 +842,12 @@
|
||||
"toggle_all": "Zaznacz wszystkie",
|
||||
"confirm_delete": "Potwierdź usunięcie tego elementu.",
|
||||
"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_manager": "Menedżer kolejki",
|
||||
@@ -1084,5 +1182,20 @@
|
||||
"dns_records_status": "Aktualny stan",
|
||||
"dns_records_type": "Typ",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div id="collapse-tab-SpamAliases" class="card-body collapse" data-bs-parent="#user-content">
|
||||
<div class="row">
|
||||
<p>{{ lang.user.spam_aliases_info|raw }}</p>
|
||||
<div class="col-md-12 col-sm-12 col-12">
|
||||
<table id="tla_table" class="table table-striped dt-responsive w-100"></table>
|
||||
</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 dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
|
||||
<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":"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"}' 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":"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"}' 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":"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","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","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","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","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","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><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>
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
- rspamd
|
||||
|
||||
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"
|
||||
depends_on:
|
||||
- redis-mailcow
|
||||
@@ -188,10 +188,10 @@ services:
|
||||
restart: always
|
||||
labels:
|
||||
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.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.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\""
|
||||
networks:
|
||||
@@ -200,7 +200,7 @@ services:
|
||||
- phpfpm
|
||||
|
||||
sogo-mailcow:
|
||||
image: ghcr.io/mailcow/sogo:1.136
|
||||
image: ghcr.io/mailcow/sogo:5.12.4
|
||||
environment:
|
||||
- DBNAME=${DBNAME}
|
||||
- DBUSER=${DBUSER}
|
||||
@@ -236,13 +236,13 @@ services:
|
||||
- sogo-userdata-backup-vol-1:/sogo_backup
|
||||
labels:
|
||||
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_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_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/cron.creds || exit 0\""
|
||||
ofelia.job-exec.sogo_backup.schedule: "@every 24h"
|
||||
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/sieve.creds || exit 0\""
|
||||
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\""
|
||||
restart: always
|
||||
networks:
|
||||
@@ -252,7 +252,7 @@ services:
|
||||
- sogo
|
||||
|
||||
dovecot-mailcow:
|
||||
image: ghcr.io/mailcow/dovecot:2.35
|
||||
image: ghcr.io/mailcow/dovecot:2.3.21.1
|
||||
depends_on:
|
||||
- mysql-mailcow
|
||||
- netfilter-mailcow
|
||||
@@ -310,22 +310,22 @@ services:
|
||||
tty: true
|
||||
labels:
|
||||
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.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_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_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_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_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_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_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\""
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
@@ -339,7 +339,7 @@ services:
|
||||
- dovecot
|
||||
|
||||
postfix-mailcow:
|
||||
image: ghcr.io/mailcow/postfix:1.81
|
||||
image: ghcr.io/mailcow/postfix:3.7.11
|
||||
depends_on:
|
||||
mysql-mailcow:
|
||||
condition: service_started
|
||||
|
||||
@@ -110,32 +110,32 @@ function backup() {
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup: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)
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup: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)
|
||||
docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup: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)
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup: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)
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup: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)
|
||||
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 ; \
|
||||
mariabackup --prepare --target-dir=/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
|
||||
;;&
|
||||
--delete-days)
|
||||
@@ -170,6 +170,19 @@ function backup() {
|
||||
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() {
|
||||
for bin in docker; do
|
||||
if [[ -z $(which ${bin}) ]]; then
|
||||
@@ -199,10 +212,17 @@ function restore() {
|
||||
case "$1" in
|
||||
vmail)
|
||||
docker stop $(docker ps -qf name=dovecot-mailcow)
|
||||
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="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz
|
||||
ARCHIVE_INFO=$(get_archive_info "backup_vmail" "${RESTORE_LOCATION}")
|
||||
if [[ -z "${ARCHIVE_INFO}" ]]; then
|
||||
echo -e "\e[31mError: No backup file found for vmail (searched for .tar.zst and .tar.gz)\e[0m"
|
||||
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)
|
||||
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:"
|
||||
@@ -218,31 +238,50 @@ function restore() {
|
||||
;;
|
||||
redis)
|
||||
docker stop $(docker ps -qf name=redis-mailcow)
|
||||
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="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz
|
||||
ARCHIVE_INFO=$(get_archive_info "backup_redis" "${RESTORE_LOCATION}")
|
||||
if [[ -z "${ARCHIVE_INFO}" ]]; then
|
||||
echo -e "\e[31mError: No backup file found for redis (searched for .tar.zst and .tar.gz)\e[0m"
|
||||
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)
|
||||
;;
|
||||
crypt)
|
||||
docker stop $(docker ps -qf name=dovecot-mailcow)
|
||||
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="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz
|
||||
ARCHIVE_INFO=$(get_archive_info "backup_crypt" "${RESTORE_LOCATION}")
|
||||
if [[ -z "${ARCHIVE_INFO}" ]]; then
|
||||
echo -e "\e[31mError: No backup file found for crypt (searched for .tar.zst and .tar.gz)\e[0m"
|
||||
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)
|
||||
;;
|
||||
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?"
|
||||
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"
|
||||
sleep 2
|
||||
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 \
|
||||
-v ${RESTORE_LOCATION}:/backup: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)
|
||||
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..."
|
||||
@@ -250,19 +289,28 @@ function restore() {
|
||||
echo -e "Skipping rspamd due to compatibility issues!\e[0m"
|
||||
else
|
||||
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 \
|
||||
-v ${RESTORE_LOCATION}:/backup: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)
|
||||
fi
|
||||
;;
|
||||
postfix)
|
||||
docker stop $(docker ps -qf name=postfix-mailcow)
|
||||
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="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz
|
||||
ARCHIVE_INFO=$(get_archive_info "backup_postfix" "${RESTORE_LOCATION}")
|
||||
if [[ -z "${ARCHIVE_INFO}" ]]; then
|
||||
echo -e "\e[31mError: No backup file found for postfix (searched for .tar.zst and .tar.gz)\e[0m"
|
||||
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)
|
||||
;;
|
||||
mysql|mariadb)
|
||||
@@ -305,14 +353,19 @@ function restore() {
|
||||
echo Restoring... && \
|
||||
gunzip < backup/backup_mysql.gz | mysql -uroot && \
|
||||
mysql -uroot -e SHUTDOWN;"
|
||||
elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \
|
||||
--entrypoint= \
|
||||
-v ${RESTORE_LOCATION}:/backup:z \
|
||||
${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \
|
||||
/bin/rm -rf /backup_mariadb/* ; \
|
||||
/bin/tar -Pxvzf /backup/backup_mariadb.tar.gz"
|
||||
else
|
||||
ARCHIVE_INFO=$(get_archive_info "backup_mariadb" "${RESTORE_LOCATION}")
|
||||
if [[ -n "${ARCHIVE_INFO}" ]]; then
|
||||
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
|
||||
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
|
||||
docker run --name mailcow-backup --rm \
|
||||
-v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \
|
||||
--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
|
||||
echo "Modifying mailcow.conf..."
|
||||
source ${RESTORE_LOCATION}/mailcow.conf
|
||||
@@ -363,8 +416,8 @@ elif [[ ${1} == "restore" ]]; then
|
||||
fi
|
||||
|
||||
echo "[ 0 ] - all"
|
||||
# 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 '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//')
|
||||
# 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 '*.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
|
||||
if [[ ${file} =~ vmail ]]; then
|
||||
echo "[ ${i} ] - Mail directory (/var/vmail)"
|
||||
|
||||
301
helper-scripts/dev_tests/test_backup_and_restore.sh
Executable file
301
helper-scripts/dev_tests/test_backup_and_restore.sh
Executable 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"
|
||||
Reference in New Issue
Block a user