From 914a8204d44be05d552ea8a9ad80ed365a749648 Mon Sep 17 00:00:00 2001 From: Marcel Schuster Date: Fri, 1 Mar 2024 23:07:05 +0100 Subject: [PATCH 01/44] Watchdog: escape subject and body for webhooks --- data/Dockerfiles/watchdog/watchdog.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index cb342c138..e0cb76a6b 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -169,9 +169,13 @@ function notify_error() { return 1 fi + # Escape subject and body (https://stackoverflow.com/a/2705678) + ESCAPED_SUBJECT=$(echo ${SUBJECT} | sed -e 's/[\/&]/\\&/g') + ESCAPED_BODY=$(echo ${BODY} | sed -e 's/[\/&]/\\&/g') + # Replace subject and body placeholders - WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s/\$SUBJECT\|\${SUBJECT}/$SUBJECT/g" | sed "s/\$BODY\|\${BODY}/$BODY/g") - + WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed -e "s/\$SUBJECT\|\${SUBJECT}/$ESCAPED_SUBJECT/g" -e "s/\$BODY\|\${BODY}/$ESCAPED_BODY/g") + # POST to webhook curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK} From 5c851f29358c634c27ec02e53364d9f7b62bb789 Mon Sep 17 00:00:00 2001 From: Ayowel Date: Tue, 26 Mar 2024 08:12:29 +0100 Subject: [PATCH 02/44] Allow prompt-less install on low-resource systems --- generate_config.sh | 64 ++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/generate_config.sh b/generate_config.sh index 05d9ee2f1..3d30a6ad1 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -147,40 +147,44 @@ done MEM_TOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo) -if [ ${MEM_TOTAL} -le "2621440" ]; then - echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations." - echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf." - read -r -p "Do you want to disable ClamAV now? [Y/n] " response - case $response in - [nN][oO]|[nN]) - SKIP_CLAMD=n +if [ -z "${SKIP_CLAMD}" ]; then + if [ ${MEM_TOTAL} -le "2621440" ]; then + echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations." + echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf." + read -r -p "Do you want to disable ClamAV now? [Y/n] " response + case $response in + [nN][oO]|[nN]) + SKIP_CLAMD=n + ;; + *) + SKIP_CLAMD=y ;; - *) - SKIP_CLAMD=y - ;; - esac -else - SKIP_CLAMD=n + esac + else + SKIP_CLAMD=n + fi fi -if [ ${MEM_TOTAL} -le "2097152" ]; then - echo "Disabling Solr on low-memory system." - SKIP_SOLR=y -elif [ ${MEM_TOTAL} -le "3670016" ]; then - echo "Installed memory is <= 3.5 GiB. It is recommended to disable Solr to prevent out-of-memory situations." - echo "Solr is a prone to run OOM and should be monitored. The default Solr heap size is 1024 MiB and should be set in mailcow.conf according to your expected load." - echo "Solr can be re-enabled by setting SKIP_SOLR=n in mailcow.conf but will refuse to start with less than 2 GB total memory." - read -r -p "Do you want to disable Solr now? [Y/n] " response - case $response in - [nN][oO]|[nN]) - SKIP_SOLR=n +if [ -z "${SKIP_SOLR}" ]; then + if [ ${MEM_TOTAL} -le "2097152" ]; then + echo "Disabling Solr on low-memory system." + SKIP_SOLR=y + elif [ ${MEM_TOTAL} -le "3670016" ]; then + echo "Installed memory is <= 3.5 GiB. It is recommended to disable Solr to prevent out-of-memory situations." + echo "Solr is a prone to run OOM and should be monitored. The default Solr heap size is 1024 MiB and should be set in mailcow.conf according to your expected load." + echo "Solr can be re-enabled by setting SKIP_SOLR=n in mailcow.conf but will refuse to start with less than 2 GB total memory." + read -r -p "Do you want to disable Solr now? [Y/n] " response + case $response in + [nN][oO]|[nN]) + SKIP_SOLR=n + ;; + *) + SKIP_SOLR=y ;; - *) - SKIP_SOLR=y - ;; - esac -else - SKIP_SOLR=n + esac + else + SKIP_SOLR=n + fi fi if [[ ${SKIP_BRANCH} != y ]]; then From ffeeb179e1894bf83513fe9630f47f27bec69ef7 Mon Sep 17 00:00:00 2001 From: Daniel Muehlbachler-Pietrzykowski Date: Wed, 3 Jul 2024 10:53:37 +0200 Subject: [PATCH 03/44] restore: remove tty requirement from restore process to allow for automated restores --- helper-scripts/backup_and_restore.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 03390927b..2977eab20 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -199,7 +199,7 @@ function restore() { case "$1" in vmail) docker stop $(docker ps -qf name=dovecot-mailcow) - docker run -it --name mailcow-backup --rm \ + 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 @@ -218,7 +218,7 @@ function restore() { ;; redis) docker stop $(docker ps -qf name=redis-mailcow) - docker run -it --name mailcow-backup --rm \ + 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 @@ -226,7 +226,7 @@ function restore() { ;; crypt) docker stop $(docker ps -qf name=dovecot-mailcow) - docker run -it --name mailcow-backup --rm \ + 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 @@ -239,7 +239,7 @@ function restore() { echo -e "Continuing anyhow. If rspamd is crashing opon 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) - docker run -it --name mailcow-backup --rm \ + 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 @@ -250,7 +250,7 @@ function restore() { echo -e "Skipping rspamd due to compatibility issues!\e[0m" else docker stop $(docker ps -qf name=rspamd-mailcow) - docker run -it --name mailcow-backup --rm \ + 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 @@ -259,7 +259,7 @@ function restore() { ;; postfix) docker stop $(docker ps -qf name=postfix-mailcow) - docker run -it --name mailcow-backup --rm \ + 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 @@ -295,7 +295,7 @@ function restore() { ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/" elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then docker run \ - -it --name mailcow-backup --rm \ + -i --name mailcow-backup --rm \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:z \ --entrypoint= \ -u mysql \ From 384e5a2e6462fa1a519f7b216821ddf7f1c8e1fe Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 9 Jul 2024 19:52:15 +0200 Subject: [PATCH 04/44] Don't expose SMTP/IMAP if announced "not provided" via SRV Fixes #5944 --- data/web/autoconfig.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/web/autoconfig.php b/data/web/autoconfig.php index 95952df0d..750463419 100644 --- a/data/web/autoconfig.php +++ b/data/web/autoconfig.php @@ -39,6 +39,9 @@ header('Content-Type: application/xml'); %EMAILADDRESS% password-cleartext + @@ -46,6 +49,7 @@ header('Content-Type: application/xml'); %EMAILADDRESS% password-cleartext + %EMAILADDRESS% password-cleartext + @@ -84,6 +91,7 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?> %EMAILADDRESS% password-cleartext + If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. From e426c3a7e7fbf80e989329853cd039a6dd482489 Mon Sep 17 00:00:00 2001 From: John Rallis Date: Mon, 29 Jul 2024 16:46:03 +0300 Subject: [PATCH 05/44] Greek names of dovecot folders Names taken from MSO 2016 --- data/conf/dovecot/dovecot.folders.conf | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/data/conf/dovecot/dovecot.folders.conf b/data/conf/dovecot/dovecot.folders.conf index 99c9670fa..fa6872679 100644 --- a/data/conf/dovecot/dovecot.folders.conf +++ b/data/conf/dovecot/dovecot.folders.conf @@ -289,5 +289,20 @@ namespace inbox { mailbox "Kladde" { special_use = \Drafts } + mailbox "Πρόχειρα" { + special_use = \Drafts + } + mailbox "Απεσταλμένα" { + special_use = \Sent + } + mailbox "Κάδος απορριμάτων" { + special_use = \Trash + } + mailbox "Ανεπιθύμητα" { + special_use = \Junk + } + mailbox "Αρχειοθετημένα" { + special_use = \Archive + } prefix = -} \ No newline at end of file +} From 2208d7e6fb2864e2ddc672104b61b2a496fc1e02 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 30 Jul 2024 14:46:08 +0200 Subject: [PATCH 06/44] [Web] add function to reset user passwords --- data/assets/templates/pw_reset_html.tpl | 29 ++ data/assets/templates/pw_reset_text.tpl | 11 + data/web/admin.php | 1 + data/web/inc/functions.inc.php | 451 +++++++++++++++++- data/web/inc/functions.mailbox.inc.php | 91 ++-- data/web/inc/init_db.inc.php | 16 +- data/web/inc/triggers.inc.php | 60 ++- data/web/inc/vars.inc.php | 6 + data/web/json_api.php | 4 +- data/web/lang/lang.de-de.json | 25 + data/web/lang/lang.en-gb.json | 25 + data/web/reset-password.php | 31 ++ data/web/templates/admin.twig | 4 +- .../admin/tab-config-password-policy.twig | 40 -- .../admin/tab-config-password-settings.twig | 102 ++++ data/web/templates/edit/mailbox.twig | 7 + data/web/templates/index.twig | 3 + data/web/templates/modals/user.twig | 27 ++ data/web/templates/reset-password.twig | 57 +++ data/web/templates/user/tab-user-auth.twig | 1 + 20 files changed, 883 insertions(+), 108 deletions(-) create mode 100644 data/assets/templates/pw_reset_html.tpl create mode 100644 data/assets/templates/pw_reset_text.tpl create mode 100644 data/web/reset-password.php delete mode 100644 data/web/templates/admin/tab-config-password-policy.twig create mode 100644 data/web/templates/admin/tab-config-password-settings.twig create mode 100644 data/web/templates/reset-password.twig diff --git a/data/assets/templates/pw_reset_html.tpl b/data/assets/templates/pw_reset_html.tpl new file mode 100644 index 000000000..481f8cbef --- /dev/null +++ b/data/assets/templates/pw_reset_html.tpl @@ -0,0 +1,29 @@ + + + + + + +Hello {{username2}},

+ +Somebody requested a new password for the {{hostname}} account associated with {{username}}.
+Date of the password reset request: {{date}}

+ +You can reset your password by clicking the link below:
+{{link}}

+ +The link will be valid for the next {{token_lifetime}} minutes.

+ +If you did not request a new password, please ignore this email.
+ + diff --git a/data/assets/templates/pw_reset_text.tpl b/data/assets/templates/pw_reset_text.tpl new file mode 100644 index 000000000..fabe1e71a --- /dev/null +++ b/data/assets/templates/pw_reset_text.tpl @@ -0,0 +1,11 @@ +Hello {{username2}}, + +Somebody requested a new password for the {{hostname}} account associated with {{username}}. +Date of the password reset request: {{date}} + +You can reset your password by clicking the link below: +{{link}} + +The link will be valid for the next {{token_lifetime}} minutes. + +If you did not request a new password, please ignore this email. diff --git a/data/web/admin.php b/data/web/admin.php index d0fcbc992..5dd7b3c6b 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -107,6 +107,7 @@ $template_data = [ 'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'], 'q_data' => quarantine('settings'), 'qn_data' => quota_notification('get'), + 'pw_reset_data' => reset_password('get_notification'), 'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'), 'rsettings' => $rsettings, 'rspamd_regex_maps' => $rspamd_regex_maps, diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 8e0ac580b..af74d1407 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1073,13 +1073,17 @@ function update_sogo_static_view($mailbox = null) { function edit_user_account($_data) { global $lang; global $pdo; + $_data_log = $_data; !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*'; !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*'; !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*'; + $username = $_SESSION['mailcow_cc_username']; $role = $_SESSION['mailcow_cc_role']; $password_old = $_data['user_old_pass']; + $pw_recovery_email = $_data['pw_recovery_email']; + if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1088,20 +1092,24 @@ function edit_user_account($_data) { ); return false; } - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` - WHERE `kind` NOT REGEXP 'location|thing|group' - AND `username` = :user"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!verify_hash($row['password'], $password_old)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { + + // edit password + if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { + $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `username` = :user"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!verify_hash($row['password'], $password_old)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $password_new = $_data['user_new_pass']; $password_new2 = $_data['user_new_pass2']; if (password_check($password_new, $password_new2) !== true) { @@ -1116,8 +1124,29 @@ function edit_user_account($_data) { ':password_hashed' => $password_hashed, ':username' => $username )); + + update_sogo_static_view(); } - update_sogo_static_view(); + // edit password recovery email + elseif (isset($pw_recovery_email)) { + if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + + $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email; + $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email) + WHERE `username` = :username"); + $stmt->execute(array( + ':recovery_email' => $pw_recovery_email, + ':username' => $username + )); + } + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -2261,6 +2290,398 @@ function uuid4() { return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } +function reset_password($action, $data = null) { + global $pdo; + global $redis; + global $mailcow_hostname; + global $PW_RESET_TOKEN_LIMIT; + global $PW_RESET_TOKEN_LIFETIME; + + $_data_log = $data; + if (isset($_data_log['new_password'])) $_data_log['new_password'] = '*'; + if (isset($_data_log['new_password2'])) $_data_log['new_password2'] = '*'; + + switch ($action) { + case 'check': + $token = $data; + + $stmt = $pdo->prepare("SELECT `t1`.`username` FROM `reset_password` AS `t1` JOIN `mailbox` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL :lifetime MINUTE) AND `t2`.`active` = 1;"); + $stmt->execute(array( + ':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token), + ':lifetime' => $PW_RESET_TOKEN_LIFETIME + )); + $return = $stmt->fetch(PDO::FETCH_ASSOC); + return empty($return['username']) ? false : $return['username']; + break; + case 'issue': + $username = $data; + + // perform cleanup + $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);"); + $stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME)); + + if (filter_var($username, FILTER_VALIDATE_EMAIL) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + + $stmt = $pdo->prepare("SELECT * FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $mailbox_data = $stmt->fetch(PDO::FETCH_ASSOC); + + if (empty($mailbox_data)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'password_reset_invalid_user' + ); + return false; + } + + $mailbox_attr = json_decode($mailbox_data['attributes'], true); + if (empty($mailbox_attr['recovery_email']) || filter_var($mailbox_attr['recovery_email'], FILTER_VALIDATE_EMAIL) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => "password_reset_invalid_user" + ); + return false; + } + + $stmt = $pdo->prepare("SELECT * FROM `reset_password` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $generated_token_count = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($generated_token_count >= $PW_RESET_TOKEN_LIMIT) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => "reset_token_limit_exceeded" + ); + return false; + } + + $token = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + + $stmt = $pdo->prepare("INSERT INTO `reset_password` (`username`, `token`) + VALUES (:username, :token)"); + $stmt->execute(array( + ':username' => $username, + ':token' => $token + )); + + $pw_reset_notification = reset_password('get_notification', 'raw'); + if (!$pw_reset_notification) return false; + + $reset_link = getBaseURL() . "/reset-password?token=" . $token; + + $request_date = new DateTime(); + $locale_date = locale_get_default(); + $date_formatter = new IntlDateFormatter( + $locale_date, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL + ); + $formatted_request_date = $date_formatter->format($request_date); + + // set template vars + // subject + $pw_reset_notification['subject'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['subject']); + $pw_reset_notification['subject'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['subject']); + $pw_reset_notification['subject'] = str_replace('{{username}}', $username, $pw_reset_notification['subject']); + $pw_reset_notification['subject'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['subject']); + $pw_reset_notification['subject'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['subject']); + $pw_reset_notification['subject'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['subject']); + // text + $pw_reset_notification['text_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['text_tmpl']); + $pw_reset_notification['text_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['text_tmpl']); + $pw_reset_notification['text_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['text_tmpl']); + $pw_reset_notification['text_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['text_tmpl']); + $pw_reset_notification['text_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['text_tmpl']); + $pw_reset_notification['text_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['text_tmpl']); + // html + $pw_reset_notification['html_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['html_tmpl']); + $pw_reset_notification['html_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['html_tmpl']); + $pw_reset_notification['html_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['html_tmpl']); + $pw_reset_notification['html_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['html_tmpl']); + $pw_reset_notification['html_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['html_tmpl']); + $pw_reset_notification['html_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['html_tmpl']); + + + $email_sent = reset_password('send_mail', array( + "from" => $pw_reset_notification['from'], + "to" => $mailbox_attr['recovery_email'], + "subject" => $pw_reset_notification['subject'], + "text" => $pw_reset_notification['text_tmpl'], + "html" => $pw_reset_notification['html_tmpl'] + )); + + if (!$email_sent){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => "recovery_email_failed" + ); + return false; + } + + list($localPart, $domainPart) = explode('@', $mailbox_attr['recovery_email']); + if (strlen($localPart) > 1) { + $maskedLocalPart = $localPart[0] . str_repeat('*', strlen($localPart) - 1); + } else { + $maskedLocalPart = "*"; + } + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => array("recovery_email_sent", $maskedLocalPart . '@' . $domainPart) + ); + return array( + "username" => $username, + "issue" => "success" + ); + break; + case 'reset': + $token = $data['token']; + $new_password = $data['new_password']; + $new_password2 = $data['new_password2']; + $username = $data['username']; + $check_tfa = $data['check_tfa']; + + if (!$username || !$token) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'invalid_reset_token' + ); + return false; + } + + # check new password + if (!password_check($new_password, $new_password2)) { + return false; + } + + if ($check_tfa){ + // check for tfa authenticators + $authenticators = get_tfa($username); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + $_SESSION['pending_mailcow_cc_username'] = $username; + $_SESSION['pending_pw_reset_token'] = $token; + $_SESSION['pending_pw_new_password'] = $new_password; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => 'awaiting_tfa_confirmation' + ); + return false; + } + } + + # set new password + $password_hashed = hash_password($new_password); + $stmt = $pdo->prepare("UPDATE `mailbox` SET + `password` = :password_hashed, + `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW()) + WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username' => $username + )); + + // perform cleanup + $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE `username` = :username;"); + $stmt->execute(array( + ':username' => $username + )); + + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'password_changed_success' + ); + return true; + break; + case 'get_notification': + $type = $data; + + try { + $settings['from'] = $redis->Get('PW_RESET_FROM'); + $settings['subject'] = $redis->Get('PW_RESET_SUBJ'); + $settings['html_tmpl'] = $redis->Get('PW_RESET_HTML'); + $settings['text_tmpl'] = $redis->Get('PW_RESET_TEXT'); + if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) { + $settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl"); + $settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl"); + } + + if ($type != "raw") { + $settings['html_tmpl'] = htmlspecialchars($settings['html_tmpl']); + $settings['text_tmpl'] = htmlspecialchars($settings['text_tmpl']); + } + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => array('redis_error', $e) + ); + return false; + } + + return $settings; + break; + case 'send_mail': + $from = $data['from']; + $to = $data['to']; + $text = $data['text']; + $html = $data['html']; + $subject = $data['subject']; + + if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'from_invalid' + ); + return false; + } + if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'to_invalid' + ); + return false; + } + if (empty($subject)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'subject_empty' + ); + return false; + } + if (empty($text)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'text_empty' + ); + return false; + } + + ini_set('max_execution_time', 0); + ini_set('max_input_time', 0); + $mail = new PHPMailer; + $mail->Timeout = 10; + $mail->SMTPOptions = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true + ) + ); + $mail->isSMTP(); + $mail->Host = 'postfix-mailcow'; + $mail->SMTPAuth = false; + $mail->Port = 25; + $mail->setFrom($from); + $mail->Subject = $subject; + $mail->CharSet ="UTF-8"; + if (!empty($html)) { + $mail->Body = $html; + $mail->AltBody = $text; + } + else { + $mail->Body = $text; + } + $mail->XMailer = 'MooMail'; + $mail->AddAddress($to); + if (!$mail->send()) { + return false; + } + $mail->ClearAllRecipients(); + + return true; + break; + } + + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + + switch ($action) { + case 'edit_notification': + $subject = $data['subject']; + $from = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $data['from']); + + if (filter_var($from, FILTER_VALIDATE_EMAIL) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => '???' + ); + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + + $text = (empty($data['text_tmpl'])) ? "" : $data['text_tmpl']; + $html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl']; + if (empty($text) && empty($html)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + + try { + $redis->Set('PW_RESET_FROM', $from); + $redis->Set('PW_RESET_SUBJ', $subject); + $redis->Set('PW_RESET_HTML', $html); + $redis->Set('PW_RESET_TEXT', $text); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => array('redis_error', $e) + ); + return false; + } + + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'saved_settings' + ); + break; + } +} + function get_logs($application, $lines = false) { if ($lines === false) { diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 00c11deec..7c9414f0a 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -2865,21 +2865,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; } if (!empty($is_now)) { - $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); - (int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); - (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); - (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); - (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); - (int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); - (int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); - (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); - $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; - $domain = $is_now['domain']; - $quota_b = $quota_m * 1048576; - $password = (!empty($_data['password'])) ? $_data['password'] : null; - $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; - $tags = (is_array($_data['tags']) ? $_data['tags'] : array()); + (int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); + (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); + (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); + (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); + (int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); + (int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); + (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); + $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; + $domain = $is_now['domain']; + $quota_b = $quota_m * 1048576; + $password = (!empty($_data['password'])) ? $_data['password'] : null; + $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; + $pw_recovery_email = (isset($_data['pw_recovery_email'])) ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email']; + $tags = (is_array($_data['tags']) ? $_data['tags'] : array()); } else { $_SESSION['return'][] = array( @@ -3132,31 +3133,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':address' => $username, ':active' => $active )); - $stmt = $pdo->prepare("UPDATE `mailbox` SET - `active` = :active, - `name`= :name, - `quota` = :quota_b, - `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), - `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), - `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), - `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), - `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), - `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), - `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access) - WHERE `username` = :username"); - $stmt->execute(array( - ':active' => $active, - ':name' => $name, - ':quota_b' => $quota_b, - ':force_pw_update' => $force_pw_update, - ':sogo_access' => $sogo_access, - ':imap_access' => $imap_access, - ':pop3_access' => $pop3_access, - ':sieve_access' => $sieve_access, - ':smtp_access' => $smtp_access, - ':relayhost' => $relayhost, - ':username' => $username - )); + try { + $stmt = $pdo->prepare("UPDATE `mailbox` SET + `active` = :active, + `name`= :name, + `quota` = :quota_b, + `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), + `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), + `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), + `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), + `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), + `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), + `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access), + `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email) + WHERE `username` = :username"); + $stmt->execute(array( + ':active' => $active, + ':name' => $name, + ':quota_b' => $quota_b, + ':force_pw_update' => $force_pw_update, + ':sogo_access' => $sogo_access, + ':imap_access' => $imap_access, + ':pop3_access' => $pop3_access, + ':sieve_access' => $sieve_access, + ':smtp_access' => $smtp_access, + ':recovery_email' => $pw_recovery_email, + ':relayhost' => $relayhost, + ':username' => $username + )); + } + catch (PDOException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => $e->getMessage() + ); + return false; + } // save tags foreach($tags as $index => $tag){ if (empty($tag)) continue; diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index f62858b3f..8c4951d57 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "26022024_1433"; + $db_version = "29072024_1000"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -483,6 +483,7 @@ function init_db_schema() { "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", + "pw_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", ), "keys" => array( "primary" => array( @@ -694,6 +695,19 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "reset_password" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "token" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + ), + "keys" => array( + "primary" => array( + "" => array("token", "created") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "imapsync" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 6922429b8..5c625e414 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -10,16 +10,54 @@ if (!empty($_GET['sso_token'])) { } } +if (isset($_POST["pw_reset_request"]) && !empty($_POST['username'])) { + reset_password("issue", $_POST['username']); + header("Location: /"); + exit; +} +if (isset($_POST["pw_reset"])) { + $username = reset_password("check", $_POST['token']); + $reset_result = reset_password("reset", array( + 'new_password' => $_POST['new_password'], + 'new_password2' => $_POST['new_password2'], + 'token' => $_POST['token'], + 'username' => $username, + 'check_tfa' => True + )); + + if ($reset_result){ + header("Location: /"); + exit; + } +} if (isset($_POST["verify_tfa_login"])) { if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { - $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; - $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); + if (isset($_SESSION['pending_mailcow_cc_username']) && isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) { + reset_password("reset", array( + 'new_password' => $_SESSION['pending_pw_new_password'], + 'new_password2' => $_SESSION['pending_pw_new_password'], + 'token' => $_SESSION['pending_pw_reset_token'], + 'username' => $_SESSION['pending_mailcow_cc_username'] + )); + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_tfa_methods']); - header("Location: /user"); + header("Location: /"); + exit; + } else { + $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; + $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + + header("Location: /user"); + } } else { + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); @@ -27,11 +65,13 @@ if (isset($_POST["verify_tfa_login"])) { } if (isset($_GET["cancel_tfa_login"])) { - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); - header("Location: /"); + header("Location: /"); } if (isset($_POST["quick_release"])) { diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 830b21805..d3165b8af 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -210,6 +210,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:'; // Show last IMAP and POP3 logins $SHOW_LAST_LOGIN = true; +// Maximum number of password reset tokens that can be generated at once per user +$PW_RESET_TOKEN_LIMIT = 3; + +// Maximum time in minutes a password reset token is valid +$PW_RESET_TOKEN_LIFETIME = 15; + // UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins // true = required // false = preferred diff --git a/data/web/json_api.php b/data/web/json_api.php index 9e165b68e..e14dd9962 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1973,7 +1973,6 @@ if (isset($_GET['query'])) { case "quota_notification_bcc": process_edit_return(quota_notification_bcc('edit', $attr)); break; - break; case "mailq": process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr))); break; @@ -2069,6 +2068,9 @@ if (isset($_GET['query'])) { case "cors": process_edit_return(cors('edit', $attr)); break; + case "reset-password-notification": + process_edit_return(reset_password('edit_notification', $attr)); + break; // return no route found if no case is matched default: http_response_code(404); diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 2298e1cbd..a734e09cb 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -14,6 +14,7 @@ "prohibited": "Untersagt durch Richtlinie", "protocol_access": "Ändern der erlaubten Protokolle", "pushover": "Pushover", + "pw_reset": "Verwalten der E-Mail zur Passwortwiederherstellung erlauben", "quarantine": "Quarantäne-Aktionen", "quarantine_attachments": "Anhänge aus Quarantäne", "quarantine_category": "Ändern der Quarantäne-Benachrichtigungskategorie", @@ -248,6 +249,11 @@ "password_policy_numbers": "Muss eine Ziffer enthalten", "password_policy_special_chars": "Muss Sonderzeichen enthalten", "password_repeat": "Passwort wiederholen", + "password_reset_info": "Wenn keine E-Mail zur Passwortwiederherstellung hinterlegt ist, kann diese Funktion nicht genutzt werden.", + "password_reset_settings": "Einstellungen zur Passwortwiederherstellung", + "password_reset_tmpl_html": "HTML Vorlage", + "password_reset_tmpl_text": "Text Vorlage", + "password_settings": "Passwort Einstellungen", "priority": "Gewichtung", "private_key": "Private Key", "quarantine": "Quarantäne", @@ -287,6 +293,8 @@ "remove_row": "Entfernen", "reset_default": "Zurücksetzen auf Standard", "reset_limit": "Hash entfernen", + "reset_password_vars": "{{link}} Der generierte Passwort-Reset-Link
{{username}} Die E-Mail-Adresse des Benutzers, der die Passwortzurücksetzung angefordert hat
{{username2}} Die E-Mail-Adresse zur Wiederherstellung
{{date}} Das Datum, an dem die Passwort-Reset-Anfrage gestellt wurde
{{token_lifetime}} Die Gültigkeitsdauer des Tokens in Minuten
{{hostname}} Der mailcow Hostname", + "restore_template": "Leer lassen, um Standard-Template wiederherzustellen.", "routing": "Routing", "rsetting_add_rule": "Regel hinzufügen", "rsetting_content": "Regelinhalt", @@ -407,6 +415,7 @@ "invalid_nexthop_authenticated": "Dieser Next Hop existiert bereits mit abweichenden Authentifizierungsdaten. Die bestehenden Authentifizierungsdaten dieses \"Next Hops\" müssen vorab angepasst werden.", "invalid_recipient_map_new": "Neuer Empfänger \"%s\" ist ungültig", "invalid_recipient_map_old": "Originaler Empfänger \"%s\" ist ungültig", + "invalid_reset_token": "Ungültiger Rücksetz-Token", "ip_list_empty": "Liste erlaubter IPs darf nicht leer sein", "is_alias": "%s lautet bereits eine Alias-Adresse", "is_alias_or_mailbox": "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden", @@ -436,6 +445,7 @@ "password_complexity": "Passwort entspricht nicht den Richtlinien", "password_empty": "Passwort darf nicht leer sein", "password_mismatch": "Passwort-Wiederholung stimmt nicht überein", + "password_reset_invalid_user": "Benutzer nicht gefunden oder keine E-Mail-Adresse zur Wiederherstellung eingerichtet", "policy_list_from_exists": "Ein Eintrag mit diesem Wert existiert bereits", "policy_list_from_invalid": "Eintrag hat ein ungültiges Format", "private_key_error": "Schlüsselfehler: %s", @@ -444,10 +454,12 @@ "pushover_token": "Pushover Token hat das falsche Format", "quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein", "recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits", + "recovery_email_failed": "E-Mail zur Wiederherstellung konnte nicht gesendet werden. Bitte wenden Sie sich an Ihren Administrator.", "redis_error": "Redis Fehler: %s", "relayhost_invalid": "Map-Eintrag %s ist ungültig", "release_send_failed": "Die Nachricht konnte nicht versendet werden: %s", "reset_f2b_regex": "Regex-Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.", + "reset_token_limit_exceeded": "Das Limit für Rücksetz-Tokens wurde überschritten. Bitte versuchen Sie es später erneut.", "resource_invalid": "Ressourcenname %s ist ungültig", "rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt", "rspamd_ui_pw_length": "Rspamd UI-Passwort muss mindestens 6 Zeichen lang sein", @@ -467,6 +479,7 @@ "tls_policy_map_dest_invalid": "Ziel ist ungültig", "tls_policy_map_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits", "tls_policy_map_parameter_invalid": "Parameter ist ungültig", + "to_invalid": "Empfänger darf nicht leer sein", "totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen", "transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits", "webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s", @@ -638,6 +651,7 @@ "nexthop": "Next Hop", "none_inherit": "Keine Auswahl / Erben", "password": "Passwort", + "password_recovery_email": "E-Mail zur Passwortwiederherstellung", "password_repeat": "Passwort wiederholen", "previous": "Vorherige Seite", "private_comment": "Privater Kommentar", @@ -741,12 +755,19 @@ "session_expires": "Die Sitzung wird in etwa 15 Sekunden beendet." }, "login": { + "back_to_mailcow": "Zurück zu mailcow", "delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.", "fido2_webauthn": "FIDO2/WebAuthn Login", + "forgot_password": "> Passwort vergessen?", + "invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.
Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.", "login": "Anmelden", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", + "new_password": "Neues Passwort", + "new_password_confirm": "Neues Passwort bestätigen", "other_logins": "Key Login", "password": "Passwort", + "reset_password": "Passwort zurücksetzen", + "request_reset_password": "Passwortänderung anfordern", "username": "Benutzername" }, "mailbox": { @@ -1065,11 +1086,13 @@ "nginx_reloaded": "Nginx wurde neu geladen", "object_modified": "Änderungen an Objekt %s wurden gespeichert", "password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert", + "password_changed_success": "Das Passwort wurde erfolgreich geändert", "pushover_settings_edited": "Pushover-Konfiguration gespeichert, bitte den Zugang im Anschluss verifizieren.", "qlearn_spam": "Nachricht-ID %s wurde als Spam gelernt und gelöscht", "queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt", "recipient_map_entry_deleted": "Empfängerumschreibung mit der ID %s wurde gelöscht", "recipient_map_entry_saved": "Empfängerumschreibung für Objekt \"%s\" wurde gespeichert", + "recovery_email_sent": "Wiederherstellungs-E-Mail an %s gesendet", "relayhost_added": "Map-Eintrag %s wurde hinzugefügt", "relayhost_removed": "Map-Eintrag %s wurde entfernt", "reset_main_logo": "Standardgrafik wurde wiederhergestellt", @@ -1202,6 +1225,7 @@ "password": "Passwort", "password_now": "Aktuelles Passwort (Änderungen bestätigen)", "password_repeat": "Passwort (Wiederholung)", + "password_reset_info": "Wenn keine E-Mail zur Passwortwiederherstellung hinterlegt ist, kann diese Funktion nicht genutzt werden.", "pushover_evaluate_x_prio": "Hohe Priorität eskalieren [X-Priority: 1]", "pushover_info": "Push-Benachrichtungen werden angewendet auf alle nicht-Spam Nachrichten zugestellt an %s, einschließlich Alias-Adressen (shared, non-shared, tagged).", "pushover_only_x_prio": "Nur Mail mit hoher Priorität berücksichtigen [X-Priority: 1]", @@ -1211,6 +1235,7 @@ "pushover_title": "Notification Titel", "pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.
Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie hängen nicht voneinander ab und werden der Reihe nach ausgeführt.
Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)", "pushover_verify": "Verbindung verifizieren", + "pw_recovery_email": "E-Mail zur Passwortwiederherstellung", "q_add_header": "Junk-Ordner", "q_all": "Alle Kategorien", "q_reject": "Abgelehnt", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 964caade6..636c1aded 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -14,6 +14,7 @@ "prohibited": "Prohibited by ACL", "protocol_access": "Change protocol access", "pushover": "Pushover", + "pw_reset": "Allow to reset mailcow user password", "quarantine": "Quarantine actions", "quarantine_attachments": "Quarantine attachments", "quarantine_category": "Change quarantine notification category", @@ -256,6 +257,11 @@ "password_policy_numbers": "Must contain at least one number", "password_policy_special_chars": "Must contain special characters", "password_repeat": "Confirmation password (repeat)", + "password_reset_info": "If no recovery email is provided, this function cannot be used.", + "password_reset_settings": "Password Recovery Settings", + "password_reset_tmpl_html": "HTML Template", + "password_reset_tmpl_text": "Text Template", + "password_settings": "Password Settings", "priority": "Priority", "private_key": "Private key", "quarantine": "Quarantine", @@ -296,6 +302,8 @@ "remove_row": "Remove row", "reset_default": "Reset to default", "reset_limit": "Remove hash", + "reset_password_vars": "{{link}} The generated password reset link
{{username}} The mailbox name of the user who requested the password reset
{{username2}} The recovery mailbox name
{{date}} The date the password reset request was made
{{token_lifetime}} The token lifetime in minutes
{{hostname}} The mailcow hostname", + "restore_template": "Leave empty to restore default template.", "routing": "Routing", "rsetting_add_rule": "Add rule", "rsetting_content": "Rule content", @@ -407,6 +415,7 @@ "invalid_nexthop_authenticated": "Next hop exists with different credentials, please update the existing credentials for this next hop first.", "invalid_recipient_map_new": "Invalid new recipient specified: %s", "invalid_recipient_map_old": "Invalid original recipient specified: %s", + "invalid_reset_token": "Invalid reset token", "ip_list_empty": "List of allowed IPs cannot be empty", "is_alias": "%s is already known as an alias address", "is_alias_or_mailbox": "%s is already known as an alias, a mailbox or an alias address expanded from an alias domain.", @@ -436,6 +445,7 @@ "password_complexity": "Password does not meet the policy", "password_empty": "Password must not be empty", "password_mismatch": "Confirmation password does not match", + "password_reset_invalid_user": "Mailbox not found or no recovery email is set", "policy_list_from_exists": "A record with given name exists", "policy_list_from_invalid": "Record has invalid format", "private_key_error": "Private key error: %s", @@ -444,10 +454,12 @@ "pushover_token": "Pushover token has a wrong format", "quota_not_0_not_numeric": "Quota must be numeric and >= 0", "recipient_map_entry_exists": "A Recipient map entry \"%s\" exists", + "recovery_email_failed": "Could not send a recovery email. Please contact your administrator.", "redis_error": "Redis error: %s", "relayhost_invalid": "Map entry %s is invalid", "release_send_failed": "Message could not be released: %s", "reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.", + "reset_token_limit_exceeded": "Reset token limit has been exceeded. Please try again later.", "resource_invalid": "Resource name %s is invalid", "rl_timeframe": "Rate limit time frame is incorrect", "rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long", @@ -470,6 +482,7 @@ "tls_policy_map_dest_invalid": "Policy destination is invalid", "tls_policy_map_entry_exists": "A TLS policy map entry \"%s\" exists", "tls_policy_map_parameter_invalid": "Policy parameter is invalid", + "to_invalid": "Recipient must not be empty", "totp_verification_failed": "TOTP verification failed", "transport_dest_exists": "Transport destination \"%s\" exists", "webauthn_verification_failed": "WebAuthn verification failed: %s", @@ -638,6 +651,7 @@ "none_inherit": "None / Inherit", "nexthop": "Next hop", "password": "Password", + "password_recovery_email": "Password recovery email", "password_repeat": "Confirmation password (repeat)", "previous": "Previous page", "private_comment": "Private comment", @@ -741,12 +755,19 @@ "session_expires": "Your session will expire in about 15 seconds" }, "login": { + "back_to_mailcow": "Back to mailcow", "delayed": "Login was delayed by %s seconds.", "fido2_webauthn": "FIDO2/WebAuthn Login", + "forgot_password": "> Forgot Password?", + "invalid_pass_reset_token": "The reset password token is invalid or has expired.
Please request a new password reset link.", "login": "Login", "mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.", + "new_password": "New Password", + "new_password_confirm": "Confirm new password", "other_logins": "Key login", "password": "Password", + "reset_password": "Reset Password", + "request_reset_password": "Request password change", "username": "Username" }, "mailbox": { @@ -1072,11 +1093,13 @@ "nginx_reloaded": "Nginx was reloaded", "object_modified": "Changes to object %s have been saved", "password_policy_saved": "Password policy was saved successfully", + "password_changed_success": "Password was successfully changed", "pushover_settings_edited": "Pushover settings successfully set, please verify credentials.", "qlearn_spam": "Message ID %s was learned as spam and deleted", "queue_command_success": "Queue command completed successfully", "recipient_map_entry_deleted": "Recipient map ID %s has been deleted", "recipient_map_entry_saved": "Recipient map entry \"%s\" has been saved", + "recovery_email_sent": "Recovery email sent to %s", "relayhost_added": "Map entry %s has been added", "relayhost_removed": "Map entry %s has been removed", "reset_main_logo": "Reset to default logo", @@ -1210,6 +1233,7 @@ "password": "Password", "password_now": "Current password (confirm changes)", "password_repeat": "Password (repeat)", + "password_reset_info": "If no email for password recovery is provided, this function cannot be used.", "pushover_evaluate_x_prio": "Escalate high priority mail [X-Priority: 1]", "pushover_info": "Push notification settings will apply to all clean (non-spam) mail delivered to %s including aliases (shared, non-shared, tagged).", "pushover_only_x_prio": "Only consider high priority mail [X-Priority: 1]", @@ -1220,6 +1244,7 @@ "pushover_sound": "Sound", "pushover_vars": "When no sender filter is defined, all mails will be considered.
Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.
Useable variables for text and title (please take note of data protection policies)", "pushover_verify": "Verify credentials", + "pw_recovery_email": "Password recovery email", "q_add_header": "Junk folder", "q_all": "All categories", "q_reject": "Rejected", diff --git a/data/web/reset-password.php b/data/web/reset-password.php new file mode 100644 index 000000000..a0225dc6a --- /dev/null +++ b/data/web/reset-password.php @@ -0,0 +1,31 @@ + str_contains($_SESSION['index_query_string'], 'mobileconfig'), + 'is_reset_token_valid' => $is_reset_token_valid, + 'reset_token' => $_GET['token'] +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/templates/admin.twig b/data/web/templates/admin.twig index 33f2422b5..0d238eee7 100644 --- a/data/web/templates/admin.twig +++ b/data/web/templates/admin.twig @@ -22,7 +22,7 @@
  • -
  • +
  • @@ -51,7 +51,7 @@ {% include 'admin/tab-config-quota.twig' %} {% include 'admin/tab-config-rsettings.twig' %} {% include 'admin/tab-config-customize.twig' %} - {% include 'admin/tab-config-password-policy.twig' %} + {% include 'admin/tab-config-password-settings.twig' %} {% include 'admin/tab-sys-mails.twig' %} {% include 'admin/tab-globalfilter-regex.twig' %} diff --git a/data/web/templates/admin/tab-config-password-policy.twig b/data/web/templates/admin/tab-config-password-policy.twig deleted file mode 100644 index 8209ba542..000000000 --- a/data/web/templates/admin/tab-config-password-policy.twig +++ /dev/null @@ -1,40 +0,0 @@ -
    -
    -
    - - {{ lang.admin.password_policy }} -
    -
    -
    - {% for name, value in password_complexity %} - {% if name == 'length' %} -
    - -
    - -
    -
    - {% else %} - -
    -
    - -
    -
    - {% endif %} - {% endfor %} -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    diff --git a/data/web/templates/admin/tab-config-password-settings.twig b/data/web/templates/admin/tab-config-password-settings.twig new file mode 100644 index 000000000..6b2c494c2 --- /dev/null +++ b/data/web/templates/admin/tab-config-password-settings.twig @@ -0,0 +1,102 @@ +
    +
    +
    + + {{ lang.admin.password_settings }} +
    +
    +
    +
    +
    + + {{ lang.admin.password_policy }} + +
    +
    +
    + {% for name, value in password_complexity %} + {% if name == 'length' %} +
    + +
    + +
    +
    + {% else %} + +
    +
    + +
    +
    + {% endif %} + {% endfor %} +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + + {{ lang.admin.password_reset_settings }} + +
    + {{ lang.admin.reset_password_vars|raw }}

    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + {{ lang.admin.password_reset_tmpl_text }} + {{ lang.admin.restore_template }} +
    +
    + +
    +
    + {{ lang.admin.password_reset_tmpl_html }} + {{ lang.admin.restore_template }} +
    +
    + +
    +
    + +
    +
    +
    +
    diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index 8960ee938..8de0095f2 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -203,6 +203,13 @@ +
    + +
    + + {{ lang.admin.password_reset_info }} +
    +
    diff --git a/data/web/templates/index.twig b/data/web/templates/index.twig index aa282547c..90e232caf 100644 --- a/data/web/templates/index.twig +++ b/data/web/templates/index.twig @@ -63,6 +63,9 @@ {% endif %}
    + {% if login_delay %}

    {{ lang.login.delayed|format(login_delay) }}

    {% endif %} diff --git a/data/web/templates/modals/user.twig b/data/web/templates/modals/user.twig index b4188773c..c9cd4b97e 100644 --- a/data/web/templates/modals/user.twig +++ b/data/web/templates/modals/user.twig @@ -309,6 +309,33 @@
    + +
    From c37bf0bb32aa58266d75ca84ac8f8f36f93d0939 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 31 Jul 2024 09:22:52 +0200 Subject: [PATCH 07/44] [Web] improve error handling for user password resets --- data/web/inc/functions.inc.php | 40 +++++++------------ data/web/lang/lang.de-de.json | 1 + data/web/lang/lang.en-gb.json | 1 + .../admin/tab-config-password-settings.twig | 8 ++-- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index af74d1407..562af71d3 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1137,7 +1137,7 @@ function edit_user_account($_data) { ); return false; } - + $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email; $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email) WHERE `username` = :username"); @@ -2329,6 +2329,17 @@ function reset_password($action, $data = null) { return false; } + $pw_reset_notification = reset_password('get_notification', 'raw'); + if (!$pw_reset_notification) return false; + if (empty($pw_reset_notification['from']) || empty($pw_reset_notification['subject'])) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $_data_log), + 'msg' => 'password_reset_na' + ); + return false; + } + $stmt = $pdo->prepare("SELECT * FROM `mailbox` WHERE `username` = :username"); $stmt->execute(array(':username' => $username)); @@ -2381,9 +2392,6 @@ function reset_password($action, $data = null) { ':token' => $token )); - $pw_reset_notification = reset_password('get_notification', 'raw'); - if (!$pw_reset_notification) return false; - $reset_link = getBaseURL() . "/reset-password?token=" . $token; $request_date = new DateTime(); @@ -2633,30 +2641,10 @@ function reset_password($action, $data = null) { $subject = $data['subject']; $from = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $data['from']); - if (filter_var($from, FILTER_VALIDATE_EMAIL) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $action, $_data_log), - 'msg' => '???' - ); - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - + $from = (!filter_var($from, FILTER_VALIDATE_EMAIL)) ? "" : $from; + $subject = (empty($subject)) ? "" : $subject; $text = (empty($data['text_tmpl'])) ? "" : $data['text_tmpl']; $html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl']; - if (empty($text) && empty($html)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } try { $redis->Set('PW_RESET_FROM', $from); diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index a734e09cb..189774eec 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -446,6 +446,7 @@ "password_empty": "Passwort darf nicht leer sein", "password_mismatch": "Passwort-Wiederholung stimmt nicht überein", "password_reset_invalid_user": "Benutzer nicht gefunden oder keine E-Mail-Adresse zur Wiederherstellung eingerichtet", + "password_reset_na": "Die Passwortwiederherstellung ist momentan nicht verfügbar. Bitte wenden Sie sich an Ihren Administrator.", "policy_list_from_exists": "Ein Eintrag mit diesem Wert existiert bereits", "policy_list_from_invalid": "Eintrag hat ein ungültiges Format", "private_key_error": "Schlüsselfehler: %s", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 636c1aded..600441809 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -446,6 +446,7 @@ "password_empty": "Password must not be empty", "password_mismatch": "Confirmation password does not match", "password_reset_invalid_user": "Mailbox not found or no recovery email is set", + "password_reset_na": "The password recovery is currently unavailable. Please contact your administrator.", "policy_list_from_exists": "A record with given name exists", "policy_list_from_invalid": "Record has invalid format", "private_key_error": "Private key error: %s", diff --git a/data/web/templates/admin/tab-config-password-settings.twig b/data/web/templates/admin/tab-config-password-settings.twig index 6b2c494c2..5998c6382 100644 --- a/data/web/templates/admin/tab-config-password-settings.twig +++ b/data/web/templates/admin/tab-config-password-settings.twig @@ -57,14 +57,14 @@
    - - + +
    - - + +
    From fbecd60e563d3e924e2c085681a5dfe976e692d9 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 31 Jul 2024 09:23:53 +0200 Subject: [PATCH 08/44] [Web] add new pw_reset acl to mailbox templates --- data/web/inc/functions.mailbox.inc.php | 12 +++++++++--- data/web/js/site/mailbox.js | 3 +++ data/web/templates/edit/mailbox-templates.twig | 1 + data/web/templates/modals/mailbox.twig | 2 ++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 7c9414f0a..ffcc38208 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -184,6 +184,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => 'global_filter_written' ); return true; + break; case 'filter': $sieve = new Sieve\SieveParser(); if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) { @@ -1249,6 +1250,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; + $_data['pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0; } else { $_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']); $_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']); @@ -1264,14 +1266,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']); $_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']); $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']); + $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']); } try { $stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, - `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) + `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`) VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, - :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) "); + :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) "); $stmt->execute(array( ':username' => $username, ':spam_alias' => $_data['spam_alias'], @@ -1287,7 +1290,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':quarantine_attachments' => $_data['quarantine_attachments'], ':quarantine_notification' => $_data['quarantine_notification'], ':quarantine_category' => $_data['quarantine_category'], - ':app_passwds' => $_data['app_passwds'] + ':app_passwds' => $_data['app_passwds'], + ':pw_reset' => $_data['pw_reset'] )); } catch (PDOException $e) { @@ -1576,6 +1580,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; + $attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0; } else { $_data['acl'] = (array)$_data['acl']; $attr['acl_spam_alias'] = 0; @@ -3276,6 +3281,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; + $attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0; } else { foreach ($is_now as $key => $value){ $attr[$key] = $is_now[$key]; diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index e2016e3ed..51dbcf435 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -380,6 +380,9 @@ $(document).ready(function() { if (template.acl_app_passwds == 1){ acl.push("app_passwds"); } + if (template.acl_pw_reset == 1){ + acl.push("pw_reset"); + } $('#user_acl').selectpicker('val', acl); $('#rl_value').val(template.rl_value); diff --git a/data/web/templates/edit/mailbox-templates.twig b/data/web/templates/edit/mailbox-templates.twig index f606bd452..6d150b263 100644 --- a/data/web/templates/edit/mailbox-templates.twig +++ b/data/web/templates/edit/mailbox-templates.twig @@ -112,6 +112,7 @@ + diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig index 6316d9fb0..0f1b23a7e 100644 --- a/data/web/templates/modals/mailbox.twig +++ b/data/web/templates/modals/mailbox.twig @@ -149,6 +149,7 @@ + @@ -318,6 +319,7 @@ + From 82fde23cc1e42136444ea45e99dc15fea35afa28 Mon Sep 17 00:00:00 2001 From: Marcel Schuster Date: Thu, 1 Aug 2024 19:14:29 +0200 Subject: [PATCH 09/44] Bump watchdog to v2.03 --- docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3efd6a42b..1e444e0d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -460,7 +460,7 @@ services: - /lib/modules:/lib/modules:ro watchdog-mailcow: - image: mailcow/watchdog:2.02 + image: mailcow/watchdog:2.03 dns: - ${IPV4_NETWORK:-172.22.1}.254 tmpfs: @@ -477,7 +477,6 @@ services: - mysql-mailcow - acme-mailcow - redis-mailcow - environment: - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} - LOG_LINES=${LOG_LINES:-9999} From 3885b07a997719ed518ee8cbabf9dc4608f5aab3 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Mon, 5 Aug 2024 19:36:55 +0200 Subject: [PATCH 10/44] [Web] Updated lang.nb-no.json (#5980) Co-authored-by: Christer Solstrand Johannessen --- data/web/lang/lang.nb-no.json | 143 ++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/data/web/lang/lang.nb-no.json b/data/web/lang/lang.nb-no.json index 830d5b423..a6a7a0ca7 100644 --- a/data/web/lang/lang.nb-no.json +++ b/data/web/lang/lang.nb-no.json @@ -322,5 +322,148 @@ "invalid_nexthop": "\"Next hop\"-format er ugyldig", "img_dimensions_exceeded": "Bildet overskriver maksimal bildestørrelse", "img_size_exceeded": "Bildet overskrider maksimal filstørrelse" + }, + "debug": { + "logs": "Logger", + "update_available": "En oppdatering er tilgjengelig", + "service": "Tjeneste", + "show_ip": "Vis offentlig IP", + "solr_dead": "Solr starter, er deaktivert eller døde", + "memory": "Minne", + "online_users": "Tilkoblede brukere", + "restart_container": "Omstart", + "size": "Størrelse", + "solr_status": "Solr-status", + "started_at": "Startet ved", + "started_on": "Startet den", + "static_logs": "Statiske logger", + "success": "Suksess", + "system_containers": "System og kontainere", + "timezone": "Tidssone", + "uptime": "Oppetid", + "no_update_available": "Systemet kjører siste versjon", + "update_failed": "Kunne ikke se etter oppdateringer", + "username": "Brukernavn", + "wip": "Foreløpig under utvikling" + }, + "diagnostics": { + "dns_records_24hours": "Vennligst vær oppmerksom på at endringer gjort i DNS kan ta opp til 24 timer før riktig status vises på denne siden. Den er ment som en måte for deg å enkelt se hvordan du kan sette opp DNS-oppføringene dine og se at alle oppføringer er korrekt lagret i DNS.", + "cname_from_a": "Verdi hentet fra A/AAAA-oppføring. Dette er støttet så lenge oppføringen peker til riktig ressurs.", + "dns_records_docs": "Vennligst også se dokumentasjonen.", + "dns_records": "DNS-oppføringer", + "dns_records_data": "Korrekte data", + "dns_records_name": "Navn", + "dns_records_status": "Nåværende status", + "dns_records_type": "Type", + "optional": "Denne oppføringen er valgfri." + }, + "edit": { + "bcc_dest_format": "BCC-destinasjon må være en enkelt, gyldig epostadresse.
    Hvis du trenger å sende en kopi til flere adresser, opprett et alias og bruk det her.", + "pushover_info": "Innstillinger for pushvarslinger vil gjelde alle rene (ikke-spam) eposter levert til %s, inkludert aliaser (delte, ikke-delte, taggede).", + "relay_transport_info": "
    Info
    Du kan definere transportmappinger for et spesifikt mål for dette domenet. Hvis dette ikke er definert, blir det gjort et MX-oppslag.", + "delete2duplicates": "Slett duplikater på målvert", + "description": "Beskrivelse", + "disable_login": "Ikke tillat innlogging (innkommende epost blir likevel mottatt)", + "domain_admin": "Endre domeneadministrator", + "max_mailboxes": "Maks antall mailbokser", + "quota_warning_bcc_info": "Advarsler blir sendt som separate kopier til de følgende mottakerne. Emnet vil få lagt til det korresponderende brukernavnet i parantes, for eksempel: Kvotevarsel (user@example.com).", + "domain_footer_skip_replies": "Ignorer bunntekst ved svar på epost", + "extended_sender_acl": "Eksterne avsenderadresser", + "extended_sender_acl_info": "En DKIM-domenenøkkel bør importeres, hvis tilgjengelig.
    \n Husk å legge denne serveren til den korresponderende SPF TXT-oppføringen.
    \n Når et domene eller aliasdomene legges til på denne serveren, og det overlapper med en ekstern adresse, blir den eksterne adressen fjernet.
    \n Bruk @domain.tld for å tillate sending som *@domain.tld.", + "password_repeat": "Bekreft passord (gjenta)", + "pushover_title": "Varslingstittel", + "pushover_vars": "Når et avsenderfilter ikke er definert, vil alle epostere bli vurdert.
    Regex-filtre såvel så eksakte avsendersjekker kan bli individuelt definert og vil bli vurdert sekvensielt. De er ikke avhengige av hverandre.
    Tilgjengelige variabler for tekst og tittel (vennligst observer policyer for databeskyttelse)", + "admin": "Endre administrator", + "domain_footer_info": "Bunntekst for hele domenet leggese til alle utgående eposter assosiert med en adresse innenfor dette domenet.
    De følgende variablene kan brukes for bunnteksten:", + "domain_footer_info_vars": { + "custom": "{= foo =} - Hvis mailboksen har en spesialattributt \"foo\" med verdi \"bar\", vil den vise \"bar\"", + "auth_user": "{= auth_user =} - Autentisert brukernavn spesifisert av en MTA", + "from_user": "{= from_user =} - Fra brukerdelen av konvolutten, f.eks. for \"moo@mailcow.tld\" vil den returnere \"moo\"", + "from_name": "{= from_name =} - Fra-navn fra konvolutten, f.eks. for \"Mailcow <moo@mailcow.tld>\" vil den vise \"Mailcow\"", + "from_addr": "{= from_addr =} - Fra adressedelen av konvolutten", + "from_domain": "{= from_domain =} - Fra domene-delen av konvolutten" + }, + "mailbox_relayhost_info": "Aktiveres kun for mailboksen og direkte aliaser, overstyrer domene-videresendingsvert.", + "mbox_rl_info": "Denne begrensningen gjelder for SASL-innloggingsnavnet, dersom det er likt noen \"from\"-adresser benyttet av den innloggede brukeren. En mailboks-begrensning overstyrer en domene-begrensning.", + "allow_from_smtp_info": "La stå tom for å tillate alle avsendere.
    IPv4/IPv6-adresser og -nettverk.", + "domain": "Endre domene", + "encryption": "Kryptering", + "exclude": "Ekskluder objekter (regex)", + "footer_exclude": "Ekskluder fra bunntekst", + "gal_info": "GAL inneholder alle objeker i et domene og kan ikke redigeres av noen brukere. Ledig/opptatt-informasjon i SOGo mangler dersom den deaktiveres! Start SOGo på nytt for å aktivere endringene.", + "grant_types": "Grant-typer", + "hostname": "Vertsnavn", + "inactive": "Inaktiv", + "kind": "Type", + "last_modified": "Sist endret", + "lookup_mx": "Målet er et regex-uttrykk for å matche mot MX_navnet (*\\.google\\.com for å route all epost som skal til en MX-server som slutter på google.com, via dette målet)", + "mailbox": "Endre mailboks", + "mailbox_quota_def": "Standardkvote for mailboks", + "max_quota": "Maks kvote pr. mailboks (MiB)", + "maxage": "Maksimal alder for meldinger, i dager, som vil hentes fra ekstern
    (0 = ignorer alder)", + "maxbytespersecond": "Maks bytes pr. sekund
    (0 = ubegrenset)", + "pushover_sender_array": "Bare vurder de følgende avsenderadressene (komma-separert)", + "pushover_sender_regex": "Vurder følgende avsender-regex", + "pushover_text": "Varslingstekst", + "pushover_sound": "Lyd", + "pushover_verify": "Bekreft identifikasjon", + "quota_mb": "Kvote (MiB)", + "quota_warning_bcc": "Kvotevarsling BCC", + "ratelimit": "Mengdebegrensning", + "domain_footer": "Bunntekst for hele domenet", + "private_comment": "Privat kommentar", + "public_comment": "Offentlig kommentar", + "client_id": "Klient-ID", + "full_name": "Fullt navn", + "gal": "Global adresseliste", + "max_aliases": "Maks. antall aliaser", + "mins_interval": "Intervall (min)", + "multiple_bookings": "Flere bookinger", + "none_inherit": "Ingen / arve", + "acl": "ACL (rettighet)", + "active": "Aktiv", + "advanced_settings": "Avanserte innstillinger", + "alias": "Endre alias", + "allow_from_smtp": "Tillat kun disse IPene å bruke SMTP", + "allowed_protocols": "Tillatte protokoller", + "app_name": "Appnavn", + "app_passwd": "App-passord", + "app_passwd_protocols": "Tillatte protokoller for app-passord", + "automap": "Prøv å automatisk mappe opp mapper (\"Sent items\", \"Sent\" => \"Sendt\" etc.)", + "backup_mx_options": "Videresendingsalternativer", + "client_secret": "Klient-hemmelighet", + "comment_info": "En privat kommentar er ikke synlig for brukeren, mens en offentlig kommentar vises som et tooltip når man holder muspekeren over det", + "created_on": "Opprettet den", + "custom_attributes": "Valgfrie attributter", + "delete1": "Slett fra kilde når fullført", + "delete2": "Slett meldinger på målvert som ikke finnes på kildeverten", + "delete_ays": "Vennligst bekreft slettingen.", + "domain_footer_html": "HTML-bunntekst", + "domain_footer_plain": "PLAIN-bunntekst", + "domain_quota": "Domenekvote", + "domains": "Domener", + "dont_check_sender_acl": "Deaktivere sendersjekk for domene %s (+ aliasdomener)", + "edit_alias_domain": "Endre aliasdomene", + "force_pw_update": "Tving endring av passord ved neste innlogging", + "force_pw_update_info": "Denne brukeren vil bare kunne logge inn på %s. App-passord kan fremdeles brukes.", + "generate": "generer", + "nexthop": "Neste hopp", + "password": "Passord", + "previous": "Forrige side", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "Eskaler mail med høy prioritet [X-Priority: 1]", + "pushover_only_x_prio": "Bare vurder epost med høy prioritet [X-Priority: 1]", + "redirect_uri": "Omdirigerings-/tilbakekallings-URL", + "relay_all": "Videresend alle mottakere", + "relay_all_info": "↪ Hvis du velger å ikke videresende alle mottakkere, så må du legge til en (\"blind\") mailboks for hver eneste mottaker det skal videresendes for.", + "relay_domain": "Videresend dette domenet", + "relay_unknown_only": "Videresend kun ikke-eksisterende mailbokser. Eksisterende mailbokser vil bli levert lokalt.", + "relayhost": "Avsender-avhengige transportmetoder", + "remove": "Fjern", + "resource": "Ressurs", + "save": "Lagre endringer", + "scope": "Omfang", + "sender_acl": "Tillat å sende som", + "sender_acl_disabled": "Avsender-sjekk er deaktivert" } } From c9187261430672f41fdb7b432444e270454278ff Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Tue, 6 Aug 2024 12:04:04 +0200 Subject: [PATCH 11/44] dovecot: fix precompiling of sieve scripts --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 8 -------- docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index b53c06bed..bd1a44f38 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -407,14 +407,6 @@ sievec /var/vmail/sieve/global_sieve_after.sieve sievec /usr/lib/dovecot/sieve/report-spam.sieve sievec /usr/lib/dovecot/sieve/report-ham.sieve -for file in /var/vmail/*/*/sieve/*.sieve ; do - if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then - continue - fi - sievec "$file" "$(dirname "$file")/../.dovecot.svbin" - chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin" -done - # Fix permissions chown root:root /etc/dovecot/sql/*.conf chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua diff --git a/docker-compose.yml b/docker-compose.yml index ece85026e..f673f1a0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -221,7 +221,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.30 + image: mailcow/dovecot:2.0 depends_on: - mysql-mailcow - netfilter-mailcow From 6ee0303b0f5b3953f66a21195eec1d08bdae1d98 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Tue, 6 Aug 2024 15:33:40 +0200 Subject: [PATCH 12/44] ui: added enotify and mime as valid options for ui --- data/web/inc/lib/sieve/extensions/enotify.xml | 33 +++++++++++ data/web/inc/lib/sieve/extensions/mime.xml | 58 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 data/web/inc/lib/sieve/extensions/enotify.xml create mode 100644 data/web/inc/lib/sieve/extensions/mime.xml diff --git a/data/web/inc/lib/sieve/extensions/enotify.xml b/data/web/inc/lib/sieve/extensions/enotify.xml new file mode 100644 index 000000000..b4686a333 --- /dev/null +++ b/data/web/inc/lib/sieve/extensions/enotify.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/web/inc/lib/sieve/extensions/mime.xml b/data/web/inc/lib/sieve/extensions/mime.xml new file mode 100644 index 000000000..d7e200f02 --- /dev/null +++ b/data/web/inc/lib/sieve/extensions/mime.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b0339372b5482781fffff6c39a286c01e5a2c65d Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 17:12:54 +0300 Subject: [PATCH 13/44] Check `mailcow.conf` exists before source it --- update.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 0c8f85fed..ddfd06de4 100755 --- a/update.sh +++ b/update.sh @@ -404,12 +404,13 @@ while (($#)); do shift done +[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;} + chmod 600 mailcow.conf source mailcow.conf detect_docker_compose_command -[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;} DOTS=${MAILCOW_HOSTNAME//[^.]}; if [ ${#DOTS} -lt 1 ]; then echo -e "\e[31mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!\e[0m" From 926af87cfb19cb3fdad8da992dccc2d506400079 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Tue, 6 Aug 2024 16:20:28 +0200 Subject: [PATCH 14/44] scripts: adding docker version check to align to docs (24.X) --- generate_config.sh | 10 ++++++++++ update.sh | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/generate_config.sh b/generate_config.sh index 4b99092b3..cc5ba1ce2 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -25,6 +25,16 @@ for bin in openssl curl docker git awk sha1sum grep cut; do if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi done +# Check Docker Version (need at least 24.X) +docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1) + +if [[ $docker_version -lt 24 ]]; then + echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" + echo -e "\e[33mmailcow needs a newer Docker version to work properly...\e[0m" + echo -e "\e[31mPlease update your Docker installation... exiting\e[0m" + exit 1 +fi + if docker compose > /dev/null 2>&1; then if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then COMPOSE_VERSION=native diff --git a/update.sh b/update.sh index 0c8f85fed..b07ebb0ad 100755 --- a/update.sh +++ b/update.sh @@ -328,6 +328,16 @@ for bin in curl docker git awk sha1sum grep cut; do fi done +# Check Docker Version (need at least 24.X) +docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1) + +if [[ $docker_version -lt 24 ]]; then + echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" + echo -e "\e[33mmailcow needs a newer Docker version to work properly... continuing on your own risk!\e[0m" + echo -e "\e[31mPlease update your Docker installation... exiting\e[0m" + sleep 10 +fi + export LC_ALL=C DATE=$(date +%Y-%m-%d_%H_%M_%S) BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD) From a001a0584f12c19efd6054b417284a5488f8eb74 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Tue, 6 Aug 2024 16:21:28 +0200 Subject: [PATCH 15/44] update.sh: fix text for min. docker ver --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index b07ebb0ad..fe4ebf890 100755 --- a/update.sh +++ b/update.sh @@ -334,7 +334,7 @@ docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1) if [[ $docker_version -lt 24 ]]; then echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" echo -e "\e[33mmailcow needs a newer Docker version to work properly... continuing on your own risk!\e[0m" - echo -e "\e[31mPlease update your Docker installation... exiting\e[0m" + echo -e "\e[31mPlease update your Docker installation... sleeping 10s\e[0m" sleep 10 fi From cc0dc2eae090fec01320d619b0008e82bebca315 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 17:51:46 +0300 Subject: [PATCH 16/44] Add color-coded error message for missing `mailcow.conf` --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index ddfd06de4..6db943a0a 100755 --- a/update.sh +++ b/update.sh @@ -404,7 +404,7 @@ while (($#)); do shift done -[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;} +[[ ! -f mailcow.conf ]] && { echo -e "\e[31mmailcow.conf is missing! Is mailcow installed?\e[0m"; exit 1;} chmod 600 mailcow.conf source mailcow.conf From e994cf4d0575aec5690663c2713f974f415db862 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 20:38:18 +0300 Subject: [PATCH 17/44] Fix typo in `update.sh`: Proceeding --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index fe4ebf890..5e1108107 100755 --- a/update.sh +++ b/update.sh @@ -434,7 +434,7 @@ elif [ ${#DOTS} -eq 1 ]; then echo "Find more information about why this message exists here: https://github.com/mailcow/mailcow-dockerized/issues/1572" read -r -p "Do you want to proceed anyway? [y/N] " response if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "OK. Procceding." + echo "OK. Proceeding." else echo "OK. Exiting." exit 1 From b3e0a6622297bc1c7d25d5d70ab84c7eab0943c7 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 21:03:17 +0300 Subject: [PATCH 18/44] Fix typo: receiving updates from `an` unsupported branch --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 5e1108107..af18e7c6e 100755 --- a/update.sh +++ b/update.sh @@ -817,7 +817,7 @@ if ! [ $NEW_BRANCH ]; then echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" else - echo -e "\e[33mYou are receiving updates from a unsupported branch.\e[0m" + echo -e "\e[33mYou are receiving updates from an unsupported branch.\e[0m" sleep 1 echo -e "\e[33mThe mailcow stack might still work but it is recommended to switch to the master branch (stable builds).\e[0m" echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" From 292306b191f952d2b853fb02458e6fc9bf066c2b Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 21:12:20 +0300 Subject: [PATCH 19/44] Fix typos and English grammar in `update.sh` German is different in using upper-case than English lol --- update.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/update.sh b/update.sh index af18e7c6e..148dfe670 100755 --- a/update.sh +++ b/update.sh @@ -828,14 +828,14 @@ elif [ $FORCE ]; then echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m" sleep 1 elif [ $NEW_BRANCH == "master" ] && [ $CURRENT_BRANCH != "master" ]; then - echo -e "\e[33mYou are about to switch your mailcow Updates to the stable (master) branch.\e[0m" + echo -e "\e[33mYou are about to switch your mailcow updates to the stable (master) branch.\e[0m" sleep 1 - echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m" + echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no data is lost...\e[0m" sleep 1 - echo -e "\e[31mWARNING: Please see on GitHub or ask in the communitys if a switch to master is stable or not. - In some rear cases a Update back to master can destroy your mailcow configuration in case of Database Upgrades etc. - Normally a upgrade back to master should be safe during each full release. - Check GitHub for Database Changes and Update only if there similar to the full release!\e[0m" + echo -e "\e[31mWARNING: Please see on GitHub or ask in the community if a switch to master is stable or not. + In some rear cases an update back to master can destroy your mailcow configuration such as database upgrade, etc. + Normally an upgrade back to master should be safe during each full release. + Check GitHub for Database changes and update only if there similar to the full release!\e[0m" read -r -p "Are you sure you that want to continue upgrading to the stable (master) branch? [y/N] " response if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then echo "OK. If you prepared yourself for that please run the update.sh Script with the --stable parameter again to trigger this process here." From 3bf90c1f73412073eca44219eb55a1c54d793406 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 21:22:30 +0300 Subject: [PATCH 20/44] Fix typo for word `Potential` in `update.sh` file. --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 148dfe670..9191055dc 100755 --- a/update.sh +++ b/update.sh @@ -982,7 +982,7 @@ if [ ! $DEV ]; then echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m" exit 1 elif [[ ${MERGE_RETURN} == 1 ]]; then - echo -e "\e[93mPotenial conflict, trying to fix...\e[0m" + echo -e "\e[93mPotential conflict, trying to fix...\e[0m" git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v git add -A git commit -m "After update on ${DATE}" > /dev/null From edd85dea8dc194092aa4ae1b60ec943213ea0928 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 6 Aug 2024 22:44:59 +0300 Subject: [PATCH 21/44] Fix `LABEL` in Dockerfile, should be key=value Refering to the [Official Docker Docs](`https://docs.docker.com/reference/dockerfile/#label`), clearly said the format of LABEL is `LABEL = = = ...`. --- data/Dockerfiles/acme/Dockerfile | 2 +- data/Dockerfiles/clamd/Dockerfile | 2 +- data/Dockerfiles/dockerapi/Dockerfile | 2 +- data/Dockerfiles/dovecot/Dockerfile | 3 ++- data/Dockerfiles/netfilter/Dockerfile | 3 ++- data/Dockerfiles/olefy/Dockerfile | 3 ++- data/Dockerfiles/phpfpm/Dockerfile | 3 ++- data/Dockerfiles/postfix/Dockerfile | 3 ++- data/Dockerfiles/rspamd/Dockerfile | 3 ++- data/Dockerfiles/sogo/Dockerfile | 3 ++- data/Dockerfiles/unbound/Dockerfile | 2 +- data/Dockerfiles/watchdog/Dockerfile | 3 ++- 12 files changed, 20 insertions(+), 12 deletions(-) diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index 8688e4400..8aa16ad58 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " +LABEL maintainer = "The Infrastructure Company GmbH " RUN apk upgrade --no-cache \ diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index ab1e2550f..1850d4bed 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " +LABEL maintainer = "The Infrastructure Company GmbH " RUN apk upgrade --no-cache \ && apk add --update --no-cache \ diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 511c46234..92c19dcc1 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " +LABEL maintainer = "The Infrastructure Company GmbH " ARG PIP_BREAK_SYSTEM_PACKAGES=1 WORKDIR /app diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 65028b173..a832bff6b 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,5 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ ARG GOSU_VERSION=1.16 diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 4f65f8e08..86f9e3f69 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -1,5 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " WORKDIR /app diff --git a/data/Dockerfiles/olefy/Dockerfile b/data/Dockerfiles/olefy/Dockerfile index e71ea9ff2..3b2729134 100644 --- a/data/Dockerfiles/olefy/Dockerfile +++ b/data/Dockerfiles/olefy/Dockerfile @@ -1,5 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " ARG PIP_BREAK_SYSTEM_PACKAGES=1 WORKDIR /app diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 22036b9b3..0ac722b22 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,5 +1,6 @@ FROM php:8.2-fpm-alpine3.18 -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?.*)$ ARG APCU_PECL_VERSION=5.1.23 diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index a45ce12b2..0f1911c62 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,5 +1,6 @@ FROM debian:bookworm-slim -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive ENV LC_ALL C diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 07f0bc36b..769562443 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,5 +1,6 @@ FROM debian:bullseye-slim -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive ARG RSPAMD_VER=rspamd_3.7.5-2~8c86c1676 diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 760c95014..6366c12ff 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,5 +1,6 @@ FROM debian:bullseye-slim -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_VERSION=bullseye diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 0ad5a05f0..958d24e5f 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " +LABEL maintainer = "The Infrastructure Company GmbH " RUN apk add --update --no-cache \ curl \ diff --git a/data/Dockerfiles/watchdog/Dockerfile b/data/Dockerfiles/watchdog/Dockerfile index a844d73bc..0f3e7dfb9 100644 --- a/data/Dockerfiles/watchdog/Dockerfile +++ b/data/Dockerfiles/watchdog/Dockerfile @@ -1,5 +1,6 @@ FROM alpine:3.20 -LABEL maintainer "The Infrastructure Company GmbH " + +LABEL maintainer = "The Infrastructure Company GmbH " # Installation RUN apk add --update \ From 8fe1cc49618b4525c8ed163092a2a3271ccbf9e7 Mon Sep 17 00:00:00 2001 From: Kasim Date: Mon, 22 Jul 2024 23:55:31 +0100 Subject: [PATCH 22/44] change nginx address #5962 --- data/Dockerfiles/acme/acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/acme/acme.sh b/data/Dockerfiles/acme/acme.sh index 9d04d10ce..f6821af44 100755 --- a/data/Dockerfiles/acme/acme.sh +++ b/data/Dockerfiles/acme/acme.sh @@ -123,7 +123,7 @@ done log_f "Database OK" log_f "Waiting for Nginx..." -until $(curl --output /dev/null --silent --head --fail http://nginx:8081); do +until $(curl --output /dev/null --silent --head --fail http://nginx.mailcowdockerized_mailcow-network:8081); do sleep 2 done log_f "Nginx OK" @@ -137,7 +137,7 @@ log_f "Resolver OK" # Waiting for domain table log_f "Waiting for domain table..." while [[ -z ${DOMAIN_TABLE} ]]; do - curl --silent http://nginx/ >/dev/null 2>&1 + curl --silent http://nginx.mailcowdockerized_mailcow-network/ >/dev/null 2>&1 DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) [[ -z ${DOMAIN_TABLE} ]] && sleep 10 done From 0cdf7647c4669eb84942e9a978f13817a5c80ac1 Mon Sep 17 00:00:00 2001 From: Kasim Date: Wed, 24 Jul 2024 23:07:42 +0100 Subject: [PATCH 23/44] Include COMPOSE_PROJECT_NAME in Nginx url --- data/Dockerfiles/acme/acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/acme/acme.sh b/data/Dockerfiles/acme/acme.sh index f6821af44..9682684e4 100755 --- a/data/Dockerfiles/acme/acme.sh +++ b/data/Dockerfiles/acme/acme.sh @@ -123,7 +123,7 @@ done log_f "Database OK" log_f "Waiting for Nginx..." -until $(curl --output /dev/null --silent --head --fail http://nginx.mailcowdockerized_mailcow-network:8081); do +until $(curl --output /dev/null --silent --head --fail http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network:8081); do sleep 2 done log_f "Nginx OK" @@ -137,7 +137,7 @@ log_f "Resolver OK" # Waiting for domain table log_f "Waiting for domain table..." while [[ -z ${DOMAIN_TABLE} ]]; do - curl --silent http://nginx.mailcowdockerized_mailcow-network/ >/dev/null 2>&1 + curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1 DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) [[ -z ${DOMAIN_TABLE} ]] && sleep 10 done From b56291f62b07df92ebff1b119379c0007bfe9d47 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 7 Aug 2024 09:50:57 +0200 Subject: [PATCH 24/44] adapt scheme to affected curl containers (dirty way... but workaround) --- data/Dockerfiles/acme/reload-configurations.sh | 14 +++++++------- data/Dockerfiles/dovecot/rspamd-pipe-ham | 6 +++--- data/Dockerfiles/dovecot/rspamd-pipe-spam | 6 +++--- data/Dockerfiles/dovecot/sa-rules.sh | 4 ++-- data/Dockerfiles/phpfpm/docker-entrypoint.sh | 10 +++++----- data/Dockerfiles/watchdog/watchdog.sh | 14 +++++++------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/data/Dockerfiles/acme/reload-configurations.sh b/data/Dockerfiles/acme/reload-configurations.sh index d5461a4db..8d194b68b 100644 --- a/data/Dockerfiles/acme/reload-configurations.sh +++ b/data/Dockerfiles/acme/reload-configurations.sh @@ -2,32 +2,32 @@ # Reading container IDs # Wrapping as array to ensure trimmed content when calling $NGINX etc. -NGINX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) -DOVECOT=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) -POSTFIX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) reload_nginx(){ echo "Reloading Nginx..." - NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) + NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) [[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; } } reload_dovecot(){ echo "Reloading Dovecot..." - DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) + DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) [[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; } } reload_postfix(){ echo "Reloading Postfix..." - POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) + POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) [[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; } } restart_container(){ for container in $*; do echo "Restarting ${container}..." - C_REST_OUT=$(curl -X POST --insecure https://dockerapi/containers/${container}/restart --silent | jq -r '.msg') + C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg') echo "${C_REST_OUT}" done } diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham index 732af8585..b9a84f1bf 100755 --- a/data/Dockerfiles/dovecot/rspamd-pipe-ham +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -3,8 +3,8 @@ FILE=/tmp/mail$$ cat > $FILE trap "/bin/rm -f $FILE" 0 1 2 3 13 15 -cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzydel -cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham -cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnham +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam index a4b91a01f..3f02c4872 100755 --- a/data/Dockerfiles/dovecot/rspamd-pipe-spam +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -3,8 +3,8 @@ FILE=/tmp/mail$$ cat > $FILE trap "/bin/rm -f $FILE" 0 1 2 3 13 15 -cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzydel -cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam -cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnspam +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd exit 0 diff --git a/data/Dockerfiles/dovecot/sa-rules.sh b/data/Dockerfiles/dovecot/sa-rules.sh index cbfeb5af2..107ea7172 100755 --- a/data/Dockerfiles/dovecot/sa-rules.sh +++ b/data/Dockerfiles/dovecot/sa-rules.sh @@ -21,11 +21,11 @@ sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then CONTAINER_NAME=rspamd-mailcow - CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \ + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \ jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \ jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") if [[ ! -z ${CONTAINER_ID} ]]; then - curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart + curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart fi fi diff --git a/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/data/Dockerfiles/phpfpm/docker-entrypoint.sh index e6c26fd19..87b4e298d 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -23,7 +23,7 @@ done # Check mysql_upgrade (master and slave) CONTAINER_ID= until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do - CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) echo "Could not get mysql-mailcow container id... trying again" sleep 2 done @@ -35,7 +35,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)" break fi - SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') + SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type) SQL_LOOP_C=$((SQL_LOOP_C+1)) echo "SQL upgrade iteration #${SQL_LOOP_C}" @@ -60,12 +60,12 @@ done # doing post-installation stuff, if SQL was upgraded (master and slave) if [ ${SQL_CHANGED} -eq 1 ]; then - POSTFIX=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) + POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then echo "Could not determine Postfix container ID, skipping Postfix restart." else echo "Restarting Postfix" - curl -X POST --silent --insecure https://dockerapi/containers/${POSTFIX}/restart | jq -r '.msg' + curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg' echo "Sleeping 5 seconds..." sleep 5 fi @@ -74,7 +74,7 @@ fi # Check mysql tz import (master and slave) TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null) if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then - SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') + SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') echo "MySQL mysql_tzinfo_to_sql - debug output:" echo ${SQL_FULL_TZINFO_IMPORT_RETURN} fi diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index cb342c138..d4de6d8db 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -191,12 +191,12 @@ get_container_ip() { else sleep 0.5 # get long container id for exact match - CONTAINER_ID=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) + CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) # returned id can have multiple elements (if scaled), shuffle for random test CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf)) if [[ ! -z ${CONTAINER_ID} ]]; then for matched_container in "${CONTAINER_ID[@]}"; do - CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) + CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) for ip_match in "${CONTAINER_IPS[@]}"; do # grep will do nothing if one of these vars is empty [[ -z ${ip_match} ]] && continue @@ -716,7 +716,7 @@ rspamd_checks() { From: watchdog@localhost Empty -' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score | sed 's/\..*//' ) +' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/scan | jq -rc .default.required_score | sed 's/\..*//' ) if [[ ${SCORE} -ne 9999 ]]; then echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2 err_count=$(( ${err_count} + 1)) @@ -1095,12 +1095,12 @@ while true; do elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then kill -STOP ${BACKGROUND_TASKS[*]} sleep 10 - CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") if [[ ! -z ${CONTAINER_ID} ]]; then if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then - HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) + HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) fi - S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) + S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) if [ ${S_RUNNING} -lt 360 ]; then log_msg "Container is running for less than 360 seconds, skipping action..." elif [[ ! -z ${HAS_INITDB} ]]; then @@ -1108,7 +1108,7 @@ while true; do sleep 60 else log_msg "Sending restart command to ${CONTAINER_ID}..." - curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart + curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart notify_error "${com_pipe_answer}" log_msg "Wait for restarted container to settle and continue watching..." sleep 35 From a4c006828e7d167b41fd9856262ac5bbbd343c14 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 7 Aug 2024 09:51:47 +0200 Subject: [PATCH 25/44] compose: bump container tags --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f673f1a0f..b58754819 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,7 +110,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.87 + image: mailcow/phpfpm:1.88 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow @@ -405,7 +405,7 @@ services: condition: service_started unbound-mailcow: condition: service_healthy - image: mailcow/acme:1.88 + image: mailcow/acme:1.89 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: From 2fe21e964126e0f4a251ad72443f21ef622c6edb Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 7 Aug 2024 14:57:36 +0300 Subject: [PATCH 26/44] Refactor: `update.sh` script with `--help` should exit with status code 0 --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 38942cc5d..b89514e58 100755 --- a/update.sh +++ b/update.sh @@ -409,7 +409,7 @@ while (($#)); do -f|--force - Force update, do not ask questions -d|--dev - Enables Developer Mode (No Checkout of update.sh for tests) ' - exit 1 + exit 0 esac shift done From 4b400eadb1112c07d8e6ac523cebf099912210e2 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 6 Jun 2024 10:30:59 +0200 Subject: [PATCH 27/44] rspamd: Added DQS RBLs when key is set --- data/Dockerfiles/rspamd/docker-entrypoint.sh | 188 ++++++++++++++ data/conf/rspamd/local.d/rbl.conf | 6 +- data/conf/rspamd/local.d/rbl_group.conf | 257 +++++++++++++++++++ 3 files changed, 450 insertions(+), 1 deletion(-) diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index 8af7619c2..a6141c57f 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -124,4 +124,192 @@ for file in /hooks/*; do fi done +# If DQS KEY is set in mailcow.conf add Spamhaus DQS RBLs +if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then + cat < /etc/rspamd/custom/dqs-rbl.conf + # Autogenerated by mailcow. DO NOT TOUCH! + rbls { + spamhaus { + rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; + from = false; + } + spamhaus_from { + from = true; + received = false; + rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; + returncodes { + SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ]; + } + } + spamhaus_authbl_received { + # Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN) + rbl = "${SPAMHAUS_DQS_KEY}.authbl.dq.spamhaus.net"; + from = false; + received = true; + ipv6 = true; + returncodes { + SH_AUTHBL_RECEIVED = "127.0.0.20" + } + } + spamhaus_dbl { + # Add checks on the HELO string + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + helo = true; + rdns = true; + dkim = true; + disable_monitoring = true; + returncodes { + RBL_DBL_SPAM = "127.0.1.2"; + RBL_DBL_PHISH = "127.0.1.4"; + RBL_DBL_MALWARE = "127.0.1.5"; + RBL_DBL_BOTNET = "127.0.1.6"; + RBL_DBL_ABUSED_SPAM = "127.0.1.102"; + RBL_DBL_ABUSED_PHISH = "127.0.1.104"; + RBL_DBL_ABUSED_MALWARE = "127.0.1.105"; + RBL_DBL_ABUSED_BOTNET = "127.0.1.106"; + RBL_DBL_DONT_QUERY_IPS = "127.0.1.255"; + } + } + spamhaus_dbl_fullurls { + ignore_defaults = true; + no_ip = true; + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + selector = 'urls:get_host' + disable_monitoring = true; + returncodes { + DBLABUSED_SPAM_FULLURLS = "127.0.1.102"; + DBLABUSED_PHISH_FULLURLS = "127.0.1.104"; + DBLABUSED_MALWARE_FULLURLS = "127.0.1.105"; + DBLABUSED_BOTNET_FULLURLS = "127.0.1.106"; + } + } + spamhaus_zrd { + # Add checks on the HELO string also for DQS + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; + helo = true; + rdns = true; + dkim = true; + disable_monitoring = true; + returncodes { + RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + RBL_ZRD_FRESH_DOMAIN = [ + "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" + ]; + RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255"; + } + } + "SPAMHAUS_ZEN_URIBL" { + enabled = true; + rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; + resolve_ip = true; + checks = ['urls']; + replyto = true; + emails = true; + ipv4 = true; + ipv6 = true; + emails_domainonly = true; + returncodes { + URIBL_SBL = "127.0.0.2"; + URIBL_SBL_CSS = "127.0.0.3"; + URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"]; + URIBL_PBL = ["127.0.0.10", "127.0.0.11"]; + URIBL_DROP = "127.0.0.9"; + } + } + SH_EMAIL_DBL { + ignore_defaults = true; + replyto = true; + emails_domainonly = true; + disable_monitoring = true; + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net" + returncodes = { + SH_EMAIL_DBL = [ + "127.0.1.2", + "127.0.1.4", + "127.0.1.5", + "127.0.1.6" + ]; + SH_EMAIL_DBL_ABUSED = [ + "127.0.1.102", + "127.0.1.104", + "127.0.1.105", + "127.0.1.106" + ]; + SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ]; + } + } + SH_EMAIL_ZRD { + ignore_defaults = true; + replyto = true; + emails_domainonly = true; + disable_monitoring = true; + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net" + returncodes = { + SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + SH_EMAIL_ZRD_FRESH_DOMAIN = [ + "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" + ]; + SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ]; + } + } + "DBL" { + # override the defaults for DBL defined in modules.d/rbl.conf + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + disable_monitoring = true; + } + "ZRD" { + ignore_defaults = true; + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; + no_ip = true; + dkim = true; + emails = true; + emails_domainonly = true; + urls = true; + returncodes = { + ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"]; + } + } + spamhaus_sbl_url { + ignore_defaults = true + rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net"; + checks = ['urls']; + disable_monitoring = true; + returncodes { + SPAMHAUS_SBL_URL = "127.0.0.2"; + } + } + + SH_HBL_EMAIL { + ignore_defaults = true; + rbl = "_email.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net"; + emails_domainonly = false; + selector = "from('smtp').lower;from('mime').lower"; + ignore_whitelist = true; + checks = ['emails', 'replyto']; + hash = "sha1"; + returncodes = { + SH_HBL_EMAIL = [ + "127.0.3.2" + ]; + } + } + + spamhaus_dqs_hbl { + symbol = "HBL_FILE_UNKNOWN"; + rbl = "_file.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net."; + selector = "attachments('rbase32', 'sha256')"; + ignore_whitelist = true; + ignore_defaults = true; + returncodes { + SH_HBL_FILE_MALICIOUS = "127.0.3.10"; + SH_HBL_FILE_SUSPICIOUS = "127.0.3.15"; + } + } + } +EOF +else + rm -rf /etc/rspamd/custom/dqs-rbl.conf +fi + exec "$@" diff --git a/data/conf/rspamd/local.d/rbl.conf b/data/conf/rspamd/local.d/rbl.conf index d49dae034..509435d5d 100644 --- a/data/conf/rspamd/local.d/rbl.conf +++ b/data/conf/rspamd/local.d/rbl.conf @@ -2,6 +2,7 @@ rbls { interserver_ip { symbol = "RBL_INTERSERVER_IP"; rbl = "rbl.interserver.net"; + from = true; ipv6 = false; returncodes { RBL_INTERSERVER_BAD_IP = "127.0.0.2"; @@ -19,4 +20,7 @@ rbls { RBL_INTERSERVER_BAD_URI = "127.0.0.2"; } } -} + +.include(try=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf" + +} \ No newline at end of file diff --git a/data/conf/rspamd/local.d/rbl_group.conf b/data/conf/rspamd/local.d/rbl_group.conf index 4d346f158..916de4ef0 100644 --- a/data/conf/rspamd/local.d/rbl_group.conf +++ b/data/conf/rspamd/local.d/rbl_group.conf @@ -17,4 +17,261 @@ symbols = { score = 4.0; description = "Listed on Interserver RBL"; } + + "SPAMHAUS_ZEN" { + weight = 7.0; + } + "SH_AUTHBL_RECEIVED" { + weight = 4.0; + } + "RBL_DBL_SPAM" { + weight = 7.0; + } + "RBL_DBL_PHISH" { + weight = 7.0; + } + "RBL_DBL_MALWARE" { + weight = 7.0; + } + "RBL_DBL_BOTNET" { + weight = 7.0; + } + "RBL_DBL_ABUSED_SPAM" { + weight = 3.0; + } + "RBL_DBL_ABUSED_PHISH" { + weight = 3.0; + } + "RBL_DBL_ABUSED_MALWARE" { + weight = 3.0; + } + "RBL_DBL_ABUSED_BOTNET" { + weight = 3.0; + } + "RBL_ZRD_VERY_FRESH_DOMAIN" { + weight = 7.0; + } + "RBL_ZRD_FRESH_DOMAIN" { + weight = 4.0; + } + "ZRD_VERY_FRESH_DOMAIN" { + weight = 7.0; + } + "ZRD_FRESH_DOMAIN" { + weight = 4.0; + } + "SH_EMAIL_DBL" { + weight = 7.0; + } + "SH_EMAIL_DBL_ABUSED" { + weight = 7.0; + } + "SH_EMAIL_ZRD_VERY_FRESH_DOMAIN" { + weight = 7.0; + } + "SH_EMAIL_ZRD_FRESH_DOMAIN" { + weight = 4.0; + } + "RBL_DBL_DONT_QUERY_IPS" { + weight = 0.0; + } + "RBL_ZRD_DONT_QUERY_IPS" { + weight = 0.0; + } + "SH_EMAIL_ZRD_DONT_QUERY_IPS" { + weight = 0.0; + } + "SH_EMAIL_DBL_DONT_QUERY_IPS" { + weight = 0.0; + } + "DBL" { + weight = 0.0; + description = "DBL unknown result"; + groups = ["spamhaus"]; + } + "DBL_SPAM" { + weight = 7; + description = "DBL uribl spam"; + groups = ["spamhaus"]; + } + "DBL_PHISH" { + weight = 7; + description = "DBL uribl phishing"; + groups = ["spamhaus"]; + } + "DBL_MALWARE" { + weight = 7; + description = "DBL uribl malware"; + groups = ["spamhaus"]; + } + "DBL_BOTNET" { + weight = 7; + description = "DBL uribl botnet C&C domain"; + groups = ["spamhaus"]; + } + + + "DBLABUSED_SPAM_FULLURLS" { + weight = 5.5; + description = "DBL uribl abused legit spam"; + groups = ["spamhaus"]; + } + "DBLABUSED_PHISH_FULLURLS" { + weight = 5.5; + description = "DBL uribl abused legit phish"; + groups = ["spamhaus"]; + } + "DBLABUSED_MALWARE_FULLURLS" { + weight = 5.5; + description = "DBL uribl abused legit malware"; + groups = ["spamhaus"]; + } + "DBLABUSED_BOTNET_FULLURLS" { + weight = 5.5; + description = "DBL uribl abused legit botnet"; + groups = ["spamhaus"]; + } + + "DBL_ABUSE" { + weight = 5.5; + description = "DBL uribl abused legit spam"; + groups = ["spamhaus"]; + } + "DBL_ABUSE_REDIR" { + weight = 1.5; + description = "DBL uribl abused spammed redirector domain"; + groups = ["spamhaus"]; + } + "DBL_ABUSE_PHISH" { + weight = 5.5; + description = "DBL uribl abused legit phish"; + groups = ["spamhaus"]; + } + "DBL_ABUSE_MALWARE" { + weight = 5.5; + description = "DBL uribl abused legit malware"; + groups = ["spamhaus"]; + } + "DBL_ABUSE_BOTNET" { + weight = 5.5; + description = "DBL uribl abused legit botnet C&C"; + groups = ["spamhaus"]; + } + "DBL_PROHIBIT" { + weight = 0.0; + description = "DBL uribl IP queries prohibited!"; + groups = ["spamhaus"]; + } + "DBL_BLOCKED_OPENRESOLVER" { + weight = 0.0; + description = "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/"; + groups = ["spamhaus"]; + } + "DBL_BLOCKED" { + weight = 0.0; + description = "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/"; + groups = ["spamhaus"]; + } + "SPAMHAUS_ZEN_URIBL" { + weight = 0.0; + description = "Spamhaus ZEN URIBL: Filtered result"; + groups = ["spamhaus"]; + } + "URIBL_SBL" { + weight = 6.5; + description = "A domain in the message body resolves to an IP listed in Spamhaus SBL"; + one_shot = true; + groups = ["spamhaus"]; + } + "URIBL_SBL_CSS" { + weight = 6.5; + description = "A domain in the message body resolves to an IP listed in Spamhaus SBL CSS"; + one_shot = true; + groups = ["spamhaus"]; + } + "URIBL_PBL" { + weight = 0.01; + description = "A domain in the message body resolves to an IP listed in Spamhaus PBL"; + one_shot = true; + groups = ["spamhaus"]; + } + "URIBL_DROP" { + weight = 6.5; + description = "A domain in the message body resolves to an IP listed in Spamhaus DROP"; + one_shot = true; + groups = ["spamhaus"]; + } + "URIBL_XBL" { + weight = 5.0; + description = "A domain in the message body resolves to an IP listed in Spamhaus XBL"; + one_shot = true; + groups = ["spamhaus"]; + } + "SPAMHAUS_SBL_URL" { + weight = 6.5; + description = "A numeric URL in the message body is listed in Spamhaus SBL"; + one_shot = true; + groups = ["spamhaus"]; + } + + "SH_HBL_EMAIL" { + weight = 7; + description = "Email listed in HBL"; + groups = ["spamhaus"]; + } + + "SH_HBL_FILE_MALICIOUS" { + weight = 7; + description = "An attachment hash is listed in Spamhaus HBL as malicious"; + groups = ["spamhaus"]; + } + + "SH_HBL_FILE_SUSPICIOUS" { + weight = 5; + description = "An attachment hash is listed in Spamhaus HBL as suspicious"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_CW_BTC" { + score = 7; + description = "Bitcoin found in Spamhaus cryptowallet list"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_CW_ETH" { + score = 7; + description = "Ethereum found in Spamhaus cryptowallet list"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_CW_BCH" { + score = 7; + description = "Bitcoinhash found in Spamhaus cryptowallet list"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_CW_XMR" { + score = 7; + description = "Monero found in Spamhaus cryptowallet list"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_CW_LTC" { + score = 7; + description = "Litecoin found in Spamhaus cryptowallet list"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_CW_XRP" { + score = 7; + description = "Ripple found in Spamhaus cryptowallet list"; + groups = ["spamhaus"]; + } + + "RBL_SPAMHAUS_HBL_URL" { + score = 7; + description = "URL found in spamhaus HBL blocklist"; + groups = ["spamhaus"]; + } + } From 5d7c9b20bc2fe101ab7da37f768d0812aec37e02 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 1 Aug 2024 12:37:31 +0200 Subject: [PATCH 28/44] rspamd: upgrade to 3.9.1 + upgrade to bookworm --- data/Dockerfiles/rspamd/Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 769562443..df15a0bed 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,11 +1,10 @@ -FROM debian:bullseye-slim - +FROM debian:bookworm-slim LABEL maintainer = "The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_3.7.5-2~8c86c1676 -ARG CODENAME=bullseye -ENV LC_ALL C +ARG RSPAMD_VER=rspamd_3.9.1-1~82f43560f +ARG CODENAME=bookworm +ENV LC_ALL=C RUN apt-get update && apt-get install -y \ tzdata \ @@ -13,11 +12,12 @@ RUN apt-get update && apt-get install -y \ gnupg2 \ apt-transport-https \ dnsutils \ - netcat \ + netcat-traditional \ wget \ redis-tools \ procps \ nano \ + lua-cjson \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ && wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\ && apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \ From b6c036496d03634c48cd953d09aeba67bf19a29d Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 1 Aug 2024 12:37:49 +0200 Subject: [PATCH 29/44] rspamd: fixed dqs rbl insertion handling --- data/Dockerfiles/rspamd/docker-entrypoint.sh | 46 ++++++++++---------- data/conf/rspamd/local.d/rbl.conf | 2 +- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index a6141c57f..cf09ee48f 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -128,7 +128,6 @@ done if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then cat < /etc/rspamd/custom/dqs-rbl.conf # Autogenerated by mailcow. DO NOT TOUCH! - rbls { spamhaus { rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; from = false; @@ -221,7 +220,7 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then replyto = true; emails_domainonly = true; disable_monitoring = true; - rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net" + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; returncodes = { SH_EMAIL_DBL = [ "127.0.1.2", @@ -243,7 +242,7 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then replyto = true; emails_domainonly = true; disable_monitoring = true; - rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net" + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; returncodes = { SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; SH_EMAIL_ZRD_FRESH_DOMAIN = [ @@ -251,26 +250,26 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then ]; SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ]; } - } - "DBL" { - # override the defaults for DBL defined in modules.d/rbl.conf - rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; - disable_monitoring = true; - } - "ZRD" { - ignore_defaults = true; - rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; - no_ip = true; - dkim = true; - emails = true; - emails_domainonly = true; - urls = true; - returncodes = { - ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; - ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"]; - } - } - spamhaus_sbl_url { + } + "DBL" { + # override the defaults for DBL defined in modules.d/rbl.conf + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + disable_monitoring = true; + } + "ZRD" { + ignore_defaults = true; + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; + no_ip = true; + dkim = true; + emails = true; + emails_domainonly = true; + urls = true; + returncodes = { + ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"]; + } + } + spamhaus_sbl_url { ignore_defaults = true rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net"; checks = ['urls']; @@ -306,7 +305,6 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then SH_HBL_FILE_SUSPICIOUS = "127.0.3.15"; } } - } EOF else rm -rf /etc/rspamd/custom/dqs-rbl.conf diff --git a/data/conf/rspamd/local.d/rbl.conf b/data/conf/rspamd/local.d/rbl.conf index 509435d5d..7f2976a08 100644 --- a/data/conf/rspamd/local.d/rbl.conf +++ b/data/conf/rspamd/local.d/rbl.conf @@ -21,6 +21,6 @@ rbls { } } -.include(try=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf" +.include(try=true,override=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf" } \ No newline at end of file From 6e00d653ce64d17092fa86c178509900aa84821b Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 1 Aug 2024 12:38:00 +0200 Subject: [PATCH 30/44] compose: bumped rspamd tag --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b58754819..baec698ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,7 @@ services: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.96 + image: mailcow/rspamd:1.97 stop_grace_period: 30s depends_on: - dovecot-mailcow @@ -90,6 +90,7 @@ services: - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-} volumes: - ./data/hooks/rspamd:/hooks:Z - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z From 52431a39425c90db35bb2a76a0078ac050fd42cb Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 7 Aug 2024 14:50:12 +0200 Subject: [PATCH 31/44] compose: bump watchdog image --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9257fe355..e1b9f0fc6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -462,7 +462,7 @@ services: - /lib/modules:/lib/modules:ro watchdog-mailcow: - image: mailcow/watchdog:2.03 + image: mailcow/watchdog:2.04 dns: - ${IPV4_NETWORK:-172.22.1}.254 tmpfs: From 7f790c5360bd40b534afd96b67ef2d127e63344a Mon Sep 17 00:00:00 2001 From: milkmaker Date: Wed, 7 Aug 2024 18:39:38 +0200 Subject: [PATCH 32/44] [Web] Updated lang.si-si.json (#5995) Co-authored-by: gomiunik --- data/web/lang/lang.si-si.json | 114 ++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json index 3a424631d..e9468e0cb 100644 --- a/data/web/lang/lang.si-si.json +++ b/data/web/lang/lang.si-si.json @@ -107,7 +107,8 @@ "post_domain_add": "SOGo container \"sogo-mailcow\" mora biti ponovno zagnan po dodajanju nove domene!

    Dodatno se mora preveriti DNS konfiguracija domene. Ko je DNS konfiguracija domene odobrena, ponovno zaženite \"acme-mailcow\" za samodejno generiranje certifikatov za novo domeno (autoconfig.<domain>, autodiscover.<domain>).
    Ta korak je opcijski in se ponovno poskuša vsakih 24 ur.", "relay_transport_info": "
    Info
    Definirate lahko preslikave transportov za cilj po meri za to domeno. Če ni nastavljena, se ustvari MX poizvedba.", "syncjob_hint": "Pozor! Gesla se morajo shraniti v plain-text!", - "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja" + "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja", + "dry": "Simuliraj sinhronizacijo" }, "admin": { "access": "Dostop", @@ -347,7 +348,10 @@ "logo_dark_label": "Za temni način", "cors_settings": "Nastavitve CORS", "allowed_methods": "Dovoljene metode za upravljanje dostopa", - "allowed_origins": "Upravljanje-dostopa-Dovoljeni-Viri" + "allowed_origins": "Upravljanje-dostopa-Dovoljeni-Viri", + "copy_to_clipboard": "Besedilo kopirano v odložišče!", + "f2b_manage_external": "Zunanje upravljanje Fail2Ban", + "f2b_manage_external_info": "Fail2ban bo še vedno vzdrževal seznam prepovedi, vendar ne bo aktivno nastavil pravil za blokiranje prometa. Uporabite spodnji ustvarjeni seznam prepovedi za zunanje blokiranje prometa." }, "danger": { "alias_goto_identical": "Alias in goto naslov morata biti identična", @@ -476,7 +480,9 @@ "temp_error": "Začasna napaka", "cors_invalid_method": "Navedena neveljavna Allow metoda", "cors_invalid_origin": "Naveden neveljaven Allow-Origin", - "invalid_recipient_map_new": "Naveden neveljaven nov prejemnik: %s" + "invalid_recipient_map_new": "Naveden neveljaven nov prejemnik: %s", + "img_dimensions_exceeded": "Slika presega največje dovoljene dimenzije", + "img_size_exceeded": "Slika presega največjo dovoljeno velikost datoteke" }, "debug": { "containers_info": "Informacije o vsebniku (containerju)", @@ -511,7 +517,13 @@ "no_update_available": "Sistem je na najnovejši verziji", "update_failed": "Ni mogoče preveriti za posodobitve", "username": "Uporabniško ime", - "wip": "Trenutno v delu" + "wip": "Trenutno v delu", + "log_info": "

    mailcow in-memory dnevniki se zbirajo v Redis seznamih in se vsako minuto omejijo na LOG_LINES (%d) da se zmanjša obremenitev.\n
    In-memory dnevniki niso namenjeni trajnemu shranjevanju. Vse aplikacije, ki beležijo dnevnike in-memory, tudi beležijo v Docker daemon in posledično v privzeti gonilnik za dnevnik.\n
    In-memory dnevniki se naj uporabljajo za odpravljanje manjših napak s containerji.

    \n

    Eksterni dnevniki se zbirajo preko API-ja posamezne aplikacije.

    \n

    Statični dnevniki so večinoma dnevniki aktivnosti, ki se ne beležijo v Dockerd, a jih je vseeno treba hraniti (razen API dnevnikov).

    ", + "login_time": "Čas", + "logs": "Dnevniki", + "memory": "Spomin", + "online_users": "Prijavljeni uporabniki", + "restart_container": "Ponovno zaženi" }, "datatables": { "infoFiltered": "(filtrirano od _MAX_ skupaj zapisov)", @@ -551,6 +563,98 @@ }, "edit": { "acl": "ACL (Dovoljenje)", - "active": "Aktivno" + "active": "Aktivno", + "allow_from_smtp": "Dovoli samo tem IP naslovom da uporabijo SMTP", + "bcc_dest_format": "Cilj BCC mora biti en veljaven email naslov.
    Če morate poslati kopijo na več naslovov, ustvarite alias in ga uporabite tukaj.", + "automap": "Poskušaj samodejno preslikati mape (\"Sent items\", \"Sent\" => \"Poslano\" ipd.)", + "admin": "Uredi skrbnika", + "domain_footer_info_vars": { + "custom": "{= foo =} - Če ima poštni predal atribut po meri \"foo\" z vrednostjo \"bar\", spremenljivka vrne \"bar\"", + "auth_user": "{= auth_user =} - Prijavljeno uporabniško ime, ki ga določi MTA", + "from_user": "{= from_user =} - leva stran email naslova uporabnika, npr. za \"moo@mailcow.tld\" vrne \"moo\"", + "from_name": "{= from_name =} - Prikazno ime, npr. za \"Mailcow <moo@mailcow.tld>\" vrne \"Mailcow\"", + "from_addr": "{= from_addr =} - e-poštni naslov \"Od\"", + "from_domain": "{= from_domain =} - domena e-poštnega naslova \"Od\"" + }, + "dont_check_sender_acl": "Onemogoči kontrolo pošiljatelja za domeno %s (+ alias domene)", + "pushover_title": "Naslov obvestila", + "domains": "Domene", + "extended_sender_acl_info": "Če je DKIM domenski ključ na voljo, ga uvozite.
    \n Ne pozabite dodati ta strežnik k ustreznemu SPF TXT zapisu.
    \n Kadar koli je domena ali alias domena dodana k tem strežniku, ki se prekriva z zunanjim naslovom, je zunanji naslov odstranjen.
    \n uporabite @domain.tld da dovolite pošiljanje kot *@domain.tld.", + "lookup_mx": "Cilj je regular expression za ujemanje MX zapisov (.*\\.google\\.com za usmeritev vse pošte na MX, ki se konča z google.com, preko tega skoka)", + "maxbytespersecond": "Največ bytov na sekundo
    (0 = neomejeno)", + "pushover_sender_array": "Upoštevaj samo sledeče e-poštne naslove pošiljateljev (ločeni z vejico)", + "mbox_rl_info": "Ta omejitev velja za SASL uporabniško ime, preverja se ujemanje s katerim koli \"from\" naslovom, ki ga uporablja prijavljeni uporabnik. Omejitev pošiljanja za poštni predal preglasi pravilo omejitve za domeno.", + "kind": "Tip", + "client_secret": "Client secret", + "comment_info": "Zasebni komentar ni viden uporabniku, javni komentar pa je viden kot tooltip v uporabnikovem pregledu.", + "created_on": "Ustvarjeno", + "custom_attributes": "Atributi po meri", + "delete1": "Izbriši na viru, ko je končano", + "delete2": "Izbriši sporočila na cilju, ki ne obstajajo na viru", + "delete2duplicates": "Izbriši dvojnike na cilju", + "delete_ays": "Prosim potrdite proces izbrisa.", + "description": "Opis", + "disable_login": "Onemogoči prijavo (dohodna pošta je še vedno sprejeta)", + "domain": "Uredi domeno", + "domain_admin": "Uredi domenskega skrbnika", + "domain_footer": "Noga za celo domeno", + "domain_footer_html": "HTML noga", + "pushover_vars": "Če ni definiran noben filter pošiljatelja, bodo upoštevana vsa sporočila.
    Regex filtre in natančna preverjanja pošiljateljev je mogoče definirati posamezno in bodo obravnavani v nadaljevanju. Niso odvisni drug od drugega.
    Uporabne spremenljivke za besedilo in naslov (prosimo, upoštevajte politike varstva podatkov)", + "pushover_verify": "Preveri poverilnice", + "quota_mb": "Omejitev (MiB)", + "quota_warning_bcc": "BCC za sporočilo z opozorilom omejitve", + "quota_warning_bcc_info": "Opozorila bodo poslana kot ločene kopije sledečim prejemnikom. K naslovu sporočila bo dodano uporabniško ime v oklepajih, npr. Opozorilo omejitve (user@example.com)", + "ratelimit": "Omejitev pošiljanja", + "advanced_settings": "Napredne nastavitve", + "allow_from_smtp_info": "Pustite prazno da dovolite vse pošiljatelje.
    IPv4/IPv6 naslovi in omrežja.", + "allowed_protocols": "Dovoljeni protokoli", + "app_name": "Ime aplikacije", + "app_passwd": "Geslo aplikacije", + "app_passwd_protocols": "Dovoljeni protokoli za geslo aplikacije", + "backup_mx_options": "Možnosti posredovanja (relay)", + "client_id": "Client ID", + "domain_footer_info": "Noge za celo domeno so dodane k vsem izhodnim e-poštnim sporočilom v tej domeni.
    V nogi se lahko uporabijo sledeče spremenljivke:", + "domain_footer_plain": "PLAIN noga", + "domain_footer_skip_replies": "Ne dodajaj noge v odgovorih na e-poštna sporočila", + "domain_quota": "Omejitev (kvota) domene", + "edit_alias_domain": "Uredi alias domeno", + "exclude": "Izključi objekte (regex)", + "extended_sender_acl": "Naslovi zunanjih pošiljateljev", + "force_pw_update": "Obvezna zamenjava gesla ob naslednji prijavi", + "force_pw_update_info": "Ta uporabnik se bo lahko prijavil samo v %s. Gesla aplikacij ostajajo v rabi.", + "footer_exclude": "Izključi iz noge", + "full_name": "Polno ime", + "gal": "Globalni seznam naslovov (GAL)", + "gal_info": "GAL vsebuje vse objekte v domeni in jih uporabniki ne morejo urejati. Če je onemogočeno, ni podatkov o o zasedenosti objekta! Ponovno zaženite SOGo za uveljavitev sprememb.", + "generate": "generiraj", + "grant_types": "Vrste dovoljenj", + "hostname": "Ime gostitelja", + "inactive": "Neaktivno", + "last_modified": "Nazadnje spremenjeno", + "mailbox": "Uredi poštni predal", + "mailbox_quota_def": "Privzeta omejitev/kvota za poštni predal", + "mailbox_relayhost_info": "Velja samo za poštni predal in neposredne aliase. Ne prepiše domenskega relay gostitelja.", + "max_aliases": "Največ aliasov", + "max_mailboxes": "Največ možnih poštnih predalov", + "max_quota": "Največja omejitev/kvota na poštni predal (MiB)", + "maxage": "Največja starost sporočil (v dnevih), po katerih bo poizvedeno iz oddaljenega vira
    (0 = ne omejuj)", + "mins_interval": "Interval (min)", + "multiple_bookings": "Več rezervacij", + "none_inherit": "Brez / podeduj", + "nexthop": "Naslednji skok", + "password": "Geslo", + "password_repeat": "Potrditev gesla (ponovite)", + "previous": "Prejšnja stran", + "private_comment": "Zasebni komentar", + "public_comment": "Javni komentar", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "Eskaliraj visoko prednostno pošto [X-Priority: 1]", + "pushover_info": "Nastavitve potisnih obvestil bodo veljala za vsa čisto (ne spam) elektronsko pošto dostavljeno v %s vključno z aliasi (deljeni, nedeljeni, označeni)", + "pushover_only_x_prio": "Upoštevaj samo pošto z visoko prioriteto [X-Priority: 1]", + "pushover_sender_regex": "Upoštevaj sledeči regex za pošiljatelja", + "pushover_text": "Besedilo obvestila", + "pushover_sound": "Zvok", + "encryption": "Šifriranje", + "alias": "Uredi alias" } } From 824a473fea3227a82e4bfc1f135b7d1459691fcb Mon Sep 17 00:00:00 2001 From: Kitof Date: Thu, 8 Aug 2024 08:42:50 +0200 Subject: [PATCH 33/44] ofelia: limit scope to mailcow project (#5776) * Filter to limit ofelia scope See https://github.com/mailcow/mailcow-dockerized/issues/5775 * compose: added ${COMPOSE_PROJECT_NAME} ENV to ofelia container --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index e1b9f0fc6..a0fb03dcd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -594,9 +594,10 @@ services: ofelia-mailcow: image: mcuadros/ofelia:latest restart: always - command: daemon --docker + command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME} environment: - TZ=${TZ} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME} depends_on: - sogo-mailcow - dovecot-mailcow From 294a406b91b44d9519cdb2ef0282cbedebdf3788 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 8 Aug 2024 09:25:52 +0200 Subject: [PATCH 34/44] fix: disabled api call to solr in ui when mailbox deleted but using flatcurve --- data/web/inc/functions.mailbox.inc.php | 2 +- docker-compose.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 00c11deec..ff8d56e2f 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -5212,7 +5212,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty' ); } - if (strtolower(getenv('SKIP_SOLR')) == 'n') { + if (strtolower(getenv('SKIP_SOLR')) == 'n' && strtolower(getenv('FLATCURVE_EXPERIMENTAL')) != 'y') { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true'); curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml')); diff --git a/docker-compose.yml b/docker-compose.yml index a0fb03dcd..0f96aeace 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -168,6 +168,7 @@ services: - DEMO_MODE=${DEMO_MODE:-n} - WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n} - CLUSTERMODE=${CLUSTERMODE:-} + - FLATCURVE_EXPERIMENTAL=${FLATCURVE_EXPERIMENTAL:-} restart: always networks: mailcow-network: From 9fee5680827d9a95e5b33a5cb64d3524ff785ee3 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Sat, 10 Aug 2024 20:44:40 +0200 Subject: [PATCH 35/44] Translations update from Weblate (#5999) * [Web] Updated lang.ru-ru.json Co-authored-by: Oleksii Kruhlenko * [Web] Updated lang.uk-ua.json Co-authored-by: Oleksii Kruhlenko --------- Co-authored-by: Oleksii Kruhlenko --- data/web/lang/lang.ru-ru.json | 14 +++++++------- data/web/lang/lang.uk-ua.json | 11 ++++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json index db52dceda..21775cb16 100644 --- a/data/web/lang/lang.ru-ru.json +++ b/data/web/lang/lang.ru-ru.json @@ -2,7 +2,7 @@ "acl": { "alias_domains": "Создание псевдонимов домена", "app_passwds": "Пароли приложений", - "bcc_maps": "Правила BBC", + "bcc_maps": "Правила BCC", "delimiter_action": "Обработка тегированной почты", "domain_desc": "Изменение описания домена", "domain_relayhost": "Изменение промежуточных узлов для домена", @@ -389,7 +389,7 @@ "imagick_exception": "Ошибка в Imagick при чтении изображения", "img_invalid": "Невозможно проверить файл изображения", "img_tmp_missing": "Невозможно проверить файл изображения: временный файл не найден", - "invalid_bcc_map_type": "Неверный тип правила BBC", + "invalid_bcc_map_type": "Неверный тип правила BCC", "invalid_destination": "Назначение \"%s\" указано неверно", "invalid_filter_type": "Неверный тип фильтра", "invalid_host": "Хост %s указан неверно", @@ -523,7 +523,7 @@ "app_passwd": "Пароль приложения", "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Параметры резервного копирования MX", - "bcc_dest_format": "Назначением для правила BBC должен быть единственный действительный адрес электронной почты.", + "bcc_dest_format": "Назначением для правила BCC должен быть единственный действительный адрес электронной почты.", "client_id": "ID клиента", "client_secret": "Секретный ключ пользователя", "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя", @@ -700,7 +700,7 @@ "add": "Добавить", "add_alias": "Добавить псевдоним", "add_alias_expand": "Скопировать псевдонимы на псевдонимы домена", - "add_bcc_entry": "Добавить правило BBC", + "add_bcc_entry": "Добавить правило BCC", "add_domain": "Добавить домен", "add_domain_alias": "Добавить псевдоним домена", "add_domain_record_first": "Пожалуйста, сначала добавьте домен", @@ -719,14 +719,14 @@ "allow_from_smtp_info": "Укажите IPv4/IPv6 адреса и/или подсети.
    Оставьте поле пустым, чтобы разрешить отправку с любых адресов.", "allowed_protocols": "Разрешенные протоколы", "backup_mx": "Резервный MX", - "bcc": "Правила BBC", + "bcc": "Правила BCC", "bcc_destination": "Назначение BCC", "bcc_destinations": "Назначение BCC", "bcc_info": "Правила BCC используются для скрытой пересылки копий всех сообщений на другой адрес. Правило типа \"получатель\" используется, когда локальный получатель выступает в качестве получателя почты. Правило типа \"отправитель\" соответствуют тому же принципу. Локальный домен не будут проинформированы о неудачной доставке.", "bcc_local_dest": "Локальный домен", - "bcc_map": "Правила BBC", + "bcc_map": "Правила BCC", "bcc_map_type": "Тип BCC", - "bcc_maps": "Правила BBC", + "bcc_maps": "Правила BCC", "bcc_rcpt_map": "Получатель", "bcc_sender_map": "Отправитель", "bcc_to_rcpt": "Переключиться на тип \"получатель\"", diff --git a/data/web/lang/lang.uk-ua.json b/data/web/lang/lang.uk-ua.json index 731224642..5cc704456 100644 --- a/data/web/lang/lang.uk-ua.json +++ b/data/web/lang/lang.uk-ua.json @@ -391,7 +391,7 @@ "last_key": "Останній ключ не можна видалити, натомість вимкніть TFA.", "login_failed": "Введено неправильний логін або пароль", "mailbox_invalid": "Неприпустима адреса поштового акаунту", - "mailbox_quota_left_exceeded": "Недостатньо вільного місця (місця залишилося: %d МіБ)", + "mailbox_quota_left_exceeded": "Недостатньо вільного місця (залишилося: %d МіБ)", "malformed_username": "Некоректне ім'я користувача", "map_content_empty": "Зміст правила не може бути порожнім", "max_alias_exceeded": "Перевищено максимальну кількість псевдонімів", @@ -478,7 +478,8 @@ "extended_sender_acl_denied": "відсутній ACL для встановлення зовнішніх адрес відправників", "template_exists": "Шаблон %s вже існує", "template_id_invalid": "Ідентифікатор шаблону %s недійсний", - "template_name_invalid": "Ім'я шаблону невірне" + "template_name_invalid": "Ім'я шаблону невірне", + "img_size_exceeded": "Зображення перевищує максимальний розмір файлу" }, "debug": { "chart_this_server": "Діаграма (цей сервер)", @@ -626,7 +627,7 @@ "admin": "Редагувати адміністратора", "allow_from_smtp": "Дозволити SMTP тільки для цих IP", "allow_from_smtp_info": "Вкажіть IPv4/IPv6 адреси та/або підмережі.
    Залиште поле порожнім, щоб дозволити відправлення з будь-яких адрес.", - "bcc_dest_format": "Призначенням правила BBC має бути єдина дійсна адреса електронної пошти.
    Якщо вам потрібно надіслати копію на кілька адрес, створіть псевдонім і використовуйте його тут.", + "bcc_dest_format": "Призначенням правила BCC має бути єдина дійсна адреса електронної пошти.
    Якщо вам потрібно надіслати копію на кілька адрес, створіть псевдонім і використовуйте його тут.", "comment_info": "Приватний коментар не видно користувачам, а публічний - відображається поряд із псевдонімом в особистому кабінеті користувача", "domain_quota": "Квота домену", "dont_check_sender_acl": "Вимкнути перевірку відправника для домену %s та псевдонімів домену", @@ -725,7 +726,7 @@ "add": "Додати", "add_alias": "Додати псевдонім", "add_alias_expand": "Копіювати псевдоніми на псевдоніми домену", - "add_bcc_entry": "Додати правило BBC", + "add_bcc_entry": "Додати правило BCC", "add_domain": "Додати домен", "add_domain_alias": "Додати псевдонім домену", "add_filter": "Додати фільтр", @@ -745,7 +746,7 @@ "bcc_local_dest": "Локальний домен", "bcc_map": "Правила ВВС", "bcc_map_type": "Тип BCC", - "bcc_maps": "Правила BBC", + "bcc_maps": "Правила BCC", "bcc_rcpt_map": "Одержувач", "bcc_sender_map": "Відправник", "bcc_to_rcpt": "Перейти на тип \"одержувач\"", From 8753ea2be61d886de220b7ceff77d98c179dc22c Mon Sep 17 00:00:00 2001 From: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:05:08 +0200 Subject: [PATCH 36/44] [Rspamd] Fix bayes config (#6000) * [Rspamd] Fix bayes config Add hint about classifier name, and add missing learn_condition * Update statistic.conf --- data/conf/rspamd/local.d/statistic.conf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/conf/rspamd/local.d/statistic.conf b/data/conf/rspamd/local.d/statistic.conf index 1ca3e0822..baa3f1c20 100644 --- a/data/conf/rspamd/local.d/statistic.conf +++ b/data/conf/rspamd/local.d/statistic.conf @@ -1,12 +1,14 @@ classifier "bayes" { + # name = "custom"; # 'name' parameter must be set if multiple classifiers are defined + learn_condition = 'return require("lua_bayes_learn").can_learn'; + new_schema = true; tokenizer { name = "osb"; } backend = "redis"; min_tokens = 11; min_learns = 5; - new_schema = true; - expire = 2592000; + expire = 7776000; statfile { symbol = "BAYES_HAM"; spam = false; From b1c1e403d2ff9993c07c5c62ba82744a69238564 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Tue, 13 Aug 2024 09:43:59 +0200 Subject: [PATCH 37/44] sogo: update to 5.11.0 + Rebase on Bookworm (#6002) * sogo: update to 5.11.0 * compose: bump sogo compose tag --- data/Dockerfiles/sogo/Dockerfile | 8 ++++---- data/Dockerfiles/sogo/syslog-ng-redis_slave.conf | 2 +- data/Dockerfiles/sogo/syslog-ng.conf | 2 +- docker-compose.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 6366c12ff..2485b6a8e 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,13 +1,13 @@ -FROM debian:bullseye-slim +FROM debian:bookworm-slim -LABEL maintainer = "The Infrastructure Company GmbH " +LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG DEBIAN_VERSION=bullseye +ARG DEBIAN_VERSION=bookworm ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ ARG GOSU_VERSION=1.17 -ENV LC_ALL C +ENV LC_ALL=C # Prerequisites RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ diff --git a/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf b/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf index 9b460bd31..7abfc4b59 100644 --- a/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf @@ -1,4 +1,4 @@ -@version: 3.28 +@version: 3.38 @include "scl.conf" options { chain_hostnames(off); diff --git a/data/Dockerfiles/sogo/syslog-ng.conf b/data/Dockerfiles/sogo/syslog-ng.conf index 889a3f328..f16a2920a 100644 --- a/data/Dockerfiles/sogo/syslog-ng.conf +++ b/data/Dockerfiles/sogo/syslog-ng.conf @@ -1,4 +1,4 @@ -@version: 3.28 +@version: 3.38 @include "scl.conf" options { chain_hostnames(off); diff --git a/docker-compose.yml b/docker-compose.yml index 0f96aeace..1df07ea15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -176,7 +176,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.123 + image: mailcow/sogo:1.124 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From b26ccc2019921aa399d801a5241dbdf21b8350c0 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Tue, 13 Aug 2024 15:59:57 +0200 Subject: [PATCH 38/44] unbound: fix healthcheck logging + added fail tolerance to checks (#6004) * unbound: fix healthcheck logging to stdout + rewrote healthcheck logic * compose: bump unbound tag * unbound: fixed healthcheck logic --- data/Dockerfiles/unbound/Dockerfile | 15 ++- data/Dockerfiles/unbound/healthcheck.sh | 128 ++++++++++++-------- data/Dockerfiles/unbound/stop-supervisor.sh | 10 ++ data/Dockerfiles/unbound/supervisord.conf | 32 +++++ data/Dockerfiles/unbound/syslog-ng.conf | 21 ++++ docker-compose.yml | 2 +- 6 files changed, 152 insertions(+), 56 deletions(-) create mode 100755 data/Dockerfiles/unbound/stop-supervisor.sh create mode 100644 data/Dockerfiles/unbound/supervisord.conf create mode 100644 data/Dockerfiles/unbound/syslog-ng.conf diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 958d24e5f..fc7b14817 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -5,14 +5,17 @@ LABEL maintainer = "The Infrastructure Company GmbH " RUN apk add --update --no-cache \ curl \ bind-tools \ + coreutils \ unbound \ bash \ openssl \ drill \ tzdata \ + syslog-ng \ + supervisor \ && curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \ && chown root:unbound /etc/unbound \ - && adduser unbound tty \ + && adduser unbound tty \ && chmod 775 /etc/unbound EXPOSE 53/udp 53/tcp @@ -21,9 +24,13 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh # healthcheck (dig, ping) COPY healthcheck.sh /healthcheck.sh +COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh + RUN chmod +x /healthcheck.sh -HEALTHCHECK --interval=30s --timeout=30s CMD [ "/healthcheck.sh" ] +HEALTHCHECK --interval=30s --timeout=10s \ + CMD sh -c '[ -f /tmp/healthcheck_status ] && [ "$(cat /tmp/healthcheck_status)" -eq 0 ] || exit 1' ENTRYPOINT ["/docker-entrypoint.sh"] - -CMD ["/usr/sbin/unbound"] +CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/unbound/healthcheck.sh b/data/Dockerfiles/unbound/healthcheck.sh index 3da98e245..7d9181127 100644 --- a/data/Dockerfiles/unbound/healthcheck.sh +++ b/data/Dockerfiles/unbound/healthcheck.sh @@ -1,76 +1,102 @@ #!/bin/bash -# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) -if [[ "${SKIP_UNBOUND_HEALTHCHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - SKIP_UNBOUND_HEALTHCHECK=y -fi +STATUS_FILE="/tmp/healthcheck_status" +RUNS=0 -# Reset logfile -echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check - logs can be found in /var/log/healthcheck.log" -echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check" > /var/log/healthcheck.log - -# Declare log function for logfile inside container -function log_to_file() { - echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" >> /var/log/healthcheck.log +# Declare log function for logfile to stdout +function log_to_stdout() { +echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" } # General Ping function to check general pingability function check_ping() { - declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9") +declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9") +local fail_tolerance=1 +local failures=0 - for ip in "${ipstoping[@]}" ; do - ping -q -c 3 -w 5 "$ip" - if [ $? -ne 0 ]; then - log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!" - log_to_file "Please check your internet connection or firewall rules to fix this error, because a simple ping test should always go through from the unbound container!" - return 1 - fi +for ip in "${ipstoping[@]}" ; do + success=false + for ((i=1; i<=3; i++)); do + ping -q -c 3 -w 5 "$ip" > /dev/null + if [ $? -eq 0 ]; then + success=true + break + else + log_to_stdout "Healthcheck: Failed to ping $ip on attempt $i. Trying again..." + fi done + + if [ "$success" = false ]; then + log_to_stdout "Healthcheck: Couldn't ping $ip after 3 attempts. Marking this IP as failed." + ((failures++)) + fi +done + +if [ $failures -gt $fail_tolerance ]; then + log_to_stdout "Healthcheck: Too many ping failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..." + return 1 +fi + +return 0 - log_to_file "Healthcheck: Ping Checks WORKING properly!" - return 0 } # General DNS Resolve Check against Unbound Resolver himself function check_dns() { - declare -a domains=("mailcow.email" "github.com" "hub.docker.com") +declare -a domains=("fuzzy.mailcow.email" "github.com" "hub.docker.com") +local fail_tolerance=1 +local failures=0 - for domain in "${domains[@]}" ; do - for ((i=1; i<=3; i++)); do - dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null - if [ $? -ne 0 ]; then - log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..." - if [ $i -eq 3 ]; then - log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!" - log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!" - return 1 - fi +for domain in "${domains[@]}" ; do + success=false + for ((i=1; i<=3; i++)); do + dig_output=$(dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 2>/dev/null) + dig_rc=$? + + if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then + log_to_stdout "Healthcheck: DNS Resolution Failed on attempt $i for $domain! Trying again..." + else + success=true + break fi - done done - - log_to_file "Healthcheck: DNS Resolver WORKING properly!" - return 0 + if [ "$success" = false ]; then + log_to_stdout "Healthcheck: DNS Resolution not possible after 3 attempts for $domain... Gave up!" + ((failures++)) + fi +done + +if [ $failures -gt $fail_tolerance ]; then + log_to_stdout "Healthcheck: Too many DNS failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..." + return 1 +fi + +return 0 } -if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then - log_to_file "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!" - exit 0 -fi +while true; do -# run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy) -check_ping + if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then + log_to_stdout "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!" + echo "0" > $STATUS_FILE + sleep 365d + fi -if [ $? -ne 0 ]; then - exit 1 -fi + # run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy) + check_ping + PING_STATUS=$? -check_dns + check_dns + DNS_STATUS=$? -if [ $? -ne 0 ]; then - exit 1 -fi + if [ $PING_STATUS -ne 0 ] || [ $DNS_STATUS -ne 0 ]; then + echo "1" > $STATUS_FILE -log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!" -exit 0 \ No newline at end of file + else + echo "0" > $STATUS_FILE + fi + + sleep 30 + +done \ No newline at end of file diff --git a/data/Dockerfiles/unbound/stop-supervisor.sh b/data/Dockerfiles/unbound/stop-supervisor.sh new file mode 100755 index 000000000..acd402738 --- /dev/null +++ b/data/Dockerfiles/unbound/stop-supervisor.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +printf "READY\n"; + +while read line; do + echo "Processing Event: $line" >&2; + kill -3 $(cat "/var/run/supervisord.pid") +done < /dev/stdin + +rm -rf /tmp/healthcheck_status \ No newline at end of file diff --git a/data/Dockerfiles/unbound/supervisord.conf b/data/Dockerfiles/unbound/supervisord.conf new file mode 100644 index 000000000..b47c8b11b --- /dev/null +++ b/data/Dockerfiles/unbound/supervisord.conf @@ -0,0 +1,32 @@ +[supervisord] +nodaemon=true +user=root +pidfile=/var/run/supervisord.pid + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true + +[program:unbound] +command=/usr/sbin/unbound +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + +[program:unbound-healthcheck] +command=/bin/bash /healthcheck.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + +[eventlistener:processes] +command=/usr/local/sbin/stop-supervisor.sh +events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL diff --git a/data/Dockerfiles/unbound/syslog-ng.conf b/data/Dockerfiles/unbound/syslog-ng.conf new file mode 100644 index 000000000..de858f9e0 --- /dev/null +++ b/data/Dockerfiles/unbound/syslog-ng.conf @@ -0,0 +1,21 @@ +@version: 4.5 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats(freq(0)); + keep_timestamp(no); + bad_hostname("^gconfd$"); +}; +source s_dgram { + unix-dgram("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +log { + source(s_dgram); + destination(d_stdout); +}; diff --git a/docker-compose.yml b/docker-compose.yml index 1df07ea15..59f417856 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: unbound-mailcow: - image: mailcow/unbound:1.22 + image: mailcow/unbound:1.23 environment: - TZ=${TZ} - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n} From a6f71faf46d5f30c762c35f5f6d16fac293a53e5 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Tue, 13 Aug 2024 16:07:09 +0200 Subject: [PATCH 39/44] github-actions: compacted auto nightly pr --- .github/ISSUE_TEMPLATE/pr_to_nighty_template.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/pr_to_nighty_template.yml b/.github/ISSUE_TEMPLATE/pr_to_nighty_template.yml index 8854ac9de..d9f878584 100644 --- a/.github/ISSUE_TEMPLATE/pr_to_nighty_template.yml +++ b/.github/ISSUE_TEMPLATE/pr_to_nighty_template.yml @@ -1,13 +1,3 @@ -## :memo: Brief description - - - - -## :computer: Commits - - - - ## :file_folder: Modified files - + \ No newline at end of file From e34afd3fdddabb82f4d71bf7e1e554d13ff21497 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 14 Aug 2024 10:02:59 +0200 Subject: [PATCH 40/44] flatcurve-fts: limit tokenizers for email adresses --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index bd1a44f38..c7564cadd 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -257,10 +257,14 @@ plugin { fts_autoindex_exclude2 = \Trash fts = flatcurve + # Maximum term length can be set via the 'maxlen' argument (maxlen is + # specified in bytes, not number of UTF-8 characters) + fts_tokenizer_email_address = maxlen=100 + fts_tokenizer_generic = algorithm=simple maxlen=30 + # These are not flatcurve settings, but required for Dovecot FTS. See # Dovecot FTS Configuration link above for further information. fts_languages = en es de - fts_tokenizer_generic = algorithm=simple fts_tokenizers = generic email-address # OPTIONAL: Recommended default FTS core configuration From 1994f706c09f7a6d0ff0c8a858e2fa0db9af17dd Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 14 Aug 2024 10:03:42 +0200 Subject: [PATCH 41/44] dovecot: optimized dockerfile syntax --- data/Dockerfiles/dovecot/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index a832bff6b..4b004cdf1 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,12 +1,12 @@ FROM alpine:3.20 -LABEL maintainer = "The Infrastructure Company GmbH " +LABEL maintainer="The Infrastructure Company GmbH " # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ ARG GOSU_VERSION=1.16 -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 # Add groups and users before installing Dovecot to not break compatibility RUN addgroup -g 5000 vmail \ @@ -133,4 +133,4 @@ COPY repl_health.sh /usr/local/bin/repl_health.sh COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh ENTRYPOINT ["/docker-entrypoint.sh"] -CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] From 6c97c4f372b35bc9237a0d9f9b5696ff24cf0797 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Thu, 15 Aug 2024 09:50:36 +0200 Subject: [PATCH 42/44] Revert "Don't expose SMTP/IMAP if announced "not provided" via SRV" --- data/web/autoconfig.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/data/web/autoconfig.php b/data/web/autoconfig.php index 750463419..95952df0d 100644 --- a/data/web/autoconfig.php +++ b/data/web/autoconfig.php @@ -39,9 +39,6 @@ header('Content-Type: application/xml'); %EMAILADDRESS% password-cleartext - @@ -49,7 +46,6 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?> %EMAILADDRESS% password-cleartext - %EMAILADDRESS% password-cleartext - @@ -91,7 +84,6 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?> %EMAILADDRESS% password-cleartext - If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. From c5e399ebc2c15f5ff2eb570894ffe37ef82bda27 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 15 Aug 2024 11:09:37 +0200 Subject: [PATCH 43/44] .github: Add pull_request_template.md --- .github/PULL_REQUEST_TEMPLATE.md | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..68ead39fa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,38 @@ + + +## Contribution Guidelines + +* [ ] I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree them + + + +## What does this PR include? + +### Short Description + + + +### Affected Containers + + + + + +## Did you run tests? + +### What did you tested? + + + +### What were the final results? (Awaited, got) + + \ No newline at end of file From e00d0d5f8de369fd8499aae78825aa4a670449e3 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 15 Aug 2024 11:32:28 +0200 Subject: [PATCH 44/44] Updated contributing.md --- CONTRIBUTING.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 623d2cabc..fae8f1d5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,42 @@ -# Contribution Guidelines (Last modified on 27th June 2024) +# Contribution Guidelines +**_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! -## Pull Requests (Last modified on 27th June 2024) +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. + +## Topics + +- [Pull Requests](#pull-requests) +- [Issue Reporting](#issue-reporting) + - [Guidelines](#issue-reporting-guidelines) + - [Issue Report Guide](#issue-report-guide) + +## Pull Requests +**_Last modified on 15th August 2024_** However, please note the following regarding pull requests: 1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow). 2. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english. -3. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case. +3. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case.* 4. **Test your changes before you commit them as a pull request.** If possible, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.* -5. 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. 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. 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! +5. **Please use** the pull request template we provide once creating a pull request. *HINT: During editing you encounter comments which looks like: ``. These can be removed or kept, as they will not rendered later on GitHub! Please only create actual content without the said comments.* +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! --- -## Issue Reporting (Last modified on 27th June 2024) +## Issue Reporting +**_Last modified on 15th August 2024_** If you plan to report a issue within mailcow please read and understand the following rules: +### 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). 2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating. 3. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english. @@ -29,7 +46,7 @@ If you plan to report a issue within mailcow please read and understand the foll 7. When you create a issue/feature request: Please note that the creation does **not guarantee an instant implementation or fix by the mailcow team or the community**. 8. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it. -### Quick guide to reporting problems: +### Issue Report Guide 1. Read your logs; follow them to see what the reason for your problem is. 2. Follow the leads given to you in your logfiles and start investigating. 3. Restarting the troubled service or the whole stack to see if the problem persists.