Browse Source

Allow making spam aliases permanent (#6888)

* Allow making spam aliases permanent

* added german translation

* updated Spamalias Twig + Rename in Spam Alias

* compose: update image tags to align to vendor version

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Josh 2 weeks ago
parent
commit
0413d26855

+ 1 - 1
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -167,7 +167,7 @@ DELIMITER //
 CREATE EVENT clean_spamalias
 ON SCHEDULE EVERY 1 DAY DO
 BEGIN
-  DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
+  DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
 END;
 //
 DELIMITER ;

+ 2 - 2
data/Dockerfiles/postfix/postfix.sh

@@ -390,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT goto FROM spamalias
   WHERE address='%s'
-    AND validity >= UNIX_TIMESTAMP()
+    AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
 EOF
 
 if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
 else
   postfix -c /opt/postfix/conf start
   sleep 126144000
-fi
+fi

+ 27 - 10
data/web/inc/functions.mailbox.inc.php

@@ -49,6 +49,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             // Default to 1 yr
             $_data["validity"] = 8760;
           }
+          if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
+            $permanent = 1;
+          }
+          else {
+            $permanent = 0;
+          }
           $domain = $_data['domain'];
           $description = $_data['description'];
           $valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
@@ -65,13 +71,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           $validity = strtotime("+" . $_data["validity"] . " hour");
-          $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES
-            (:address, :description, :goto, :validity)");
+          $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES
+            (:address, :description, :goto, :validity, :permanent)");
           $stmt->execute(array(
             ':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain,
             ':description' => $description,
             ':goto' => $username,
-            ':validity' => $validity
+            ':validity' => $validity,
+            ':permanent' => $permanent
           ));
           $_SESSION['return'][] = array(
             'type' => 'success',
@@ -2103,15 +2110,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            if (empty($_data['validity'])) {
+            if (empty($_data['validity']) && empty($_data['permanent'])) {
               continue;
             }
-            $validity = round((int)time() + ($_data['validity'] * 3600));
-            $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
+            if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) {
+              $permanent = 1;
+              $validity = 0;
+            }
+            else if (isset($_data['validity'])) {
+              $permanent = 0;
+              $validity = round((int)time() + ($_data['validity'] * 3600));
+            }
+            $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE
               `address` = :address");
             $stmt->execute(array(
               ':address' => $address,
-              ':validity' => $validity
+              ':validity' => $validity,
+              ':permanent' => $permanent
             ));
             $_SESSION['return'][] = array(
               'type' => 'success',
@@ -4584,10 +4599,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             `description`,
             `validity`,
             `created`,
-            `modified`
+            `modified`,
+            `permanent`
               FROM `spamalias`
                 WHERE `goto` = :username
-                  AND `validity` >= :unixnow");
+                  AND (`validity` >= :unixnow
+                    OR `permanent` != 0)");
           $stmt->execute(array(':username' => $_data, ':unixnow' => time()));
           $tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
           return $tladata;
@@ -5162,7 +5179,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username");
             $stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
             $MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
-            $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow");
+            $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)");
             $stmt->execute(array(':address' => $_data, ':unixnow' => time()));
             $SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
             $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];

+ 3 - 2
data/web/inc/init_db.inc.php

@@ -4,7 +4,7 @@ function init_db_schema()
   try {
     global $pdo;
 
-    $db_version = "07102025_1015";
+    $db_version = "10312025_0525";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -554,7 +554,8 @@ function init_db_schema()
           "description" => "TEXT NOT NULL",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
-          "validity" => "INT(11)"
+          "validity" => "INT(11)",
+          "permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
         ),
         "keys" => array(
           "primary" => array(

+ 19 - 3
data/web/js/site/user.js

@@ -175,6 +175,10 @@ jQuery(function($){
                 '</div>';
               item.chkbox = '<input type="checkbox" class="form-check-input" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />';
               item.address = escapeHtml(item.address);
+              item.validity = {
+                value: item.validity,
+                permanent: item.permanent
+              };
             }
             else {
               item.chkbox = '<input type="checkbox" class="form-check-input" disabled />';
@@ -218,9 +222,21 @@ jQuery(function($){
           title: lang.alias_valid_until,
           data: 'validity',
           defaultContent: '',
-          createdCell: function(td, cellData) {
-            createSortableDate(td, cellData)
-          }
+          render: function (data, type) {
+            var date = new Date(data.value ? data.value * 1000 : 0);
+            switch (type) {
+              case "sort":
+                if (data.permanent) {
+                  return 0;
+                }
+                return date.getTime();
+              default:
+                if (data.permanent) {
+                  return lang.forever;
+                }
+                return date.toLocaleDateString(LOCALE, DATETIME_FORMAT);
+            }
+          },
         },
         {
           title: lang.created_on,

+ 5 - 2
data/web/lang/lang.de-de.json

@@ -987,7 +987,7 @@
         "sogo_visible": "Alias Sichtbarkeit in SOGo",
         "sogo_visible_n": "Alias in SOGo verbergen",
         "sogo_visible_y": "Alias in SOGo anzeigen",
-        "spam_aliases": "Temp. Alias",
+        "spam_aliases": "Spam-Alias",
         "stats": "Statistik",
         "status": "Status",
         "sync_jobs": "Synchronisationen",
@@ -1281,7 +1281,9 @@
         "encryption": "Verschlüsselung",
         "excludes": "Ausschlüsse",
         "expire_in": "Ungültig in",
+        "expire_never": "Niemals ungültig",
         "fido2_webauthn": "FIDO2/WebAuthn",
+        "forever": "Für immer",
         "force_pw_update": "Das Passwort für diesen Benutzer <b>muss</b> geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.",
         "from": "von",
         "generate": "generieren",
@@ -1346,7 +1348,8 @@
         "sogo_profile_reset": "SOGo-Profil zurücksetzen",
         "sogo_profile_reset_help": "Das Profil wird inklusive <b>aller</b> Kalender- und Kontaktdaten <b>unwiederbringlich gelöscht</b>.",
         "sogo_profile_reset_now": "Profil jetzt zurücksetzen",
-        "spam_aliases": "Temporäre E-Mail-Aliasse",
+        "spam_aliases": "Spam E-Mail-Aliasse",
+        "spam_aliases_info": "Ein Spam-Alias ist eine temporäre E-Mailadresse, die benutzt werden kann, um eine echte E-Mail Adressen zu schützen. <br>Optional kann eine Ablaufzeit gesetzt werden, sodass der Alias nach dem definierten Zeitraum automatisch deaktiviert wird, was missbrauchte oder geleakte Adressen effektiv entsorgt.",
         "spam_score_reset": "Auf Server-Standard zurücksetzen",
         "spamfilter": "Spamfilter",
         "spamfilter_behavior": "Bewertung",

+ 4 - 1
data/web/lang/lang.en-gb.json

@@ -1288,7 +1288,9 @@
         "encryption": "Encryption",
         "excludes": "Excludes",
         "expire_in": "Expire in",
+        "expire_never": "Never Expire",
         "fido2_webauthn": "FIDO2/WebAuthn",
+        "forever": "Forever",
         "force_pw_update": "You <b>must</b> set a new password to be able to access groupware related services.",
         "from": "from",
         "generate": "generate",
@@ -1355,7 +1357,8 @@
         "sogo_profile_reset": "Reset SOGo profile",
         "sogo_profile_reset_help": "This will destroy a user's SOGo profile and <b>delete all contact and calendar data irretrievable</b>.",
         "sogo_profile_reset_now": "Reset profile now",
-        "spam_aliases": "Temporary email aliases",
+        "spam_aliases": "Spam email aliases",
+        "spam_aliases_info": "A spam alias is a temporary email address that can be used to protect real email addresses. <br>Optionally, an expiration time can be set so that the alias is automatically deactivated after the defined period, effectively disposing of abused or leaked addresses.",
         "spam_score_reset": "Reset to server default",
         "spamfilter": "Spam filter",
         "spamfilter_behavior": "Rating",

+ 6 - 1
data/web/lang/lang.es-es.json

@@ -1084,6 +1084,7 @@
         "aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)",
         "change_password": "Cambiar contraseña",
         "create_syncjob": "Crear nuevo trabajo de sincronización",
+        "created_on": "Creado",
         "daily": "Cada día",
         "day": "Día",
         "description": "Descripción",
@@ -1095,6 +1096,9 @@
         "edit": "Editar",
         "encryption": "Cifrado",
         "excludes": "Excluye",
+        "expire_in": "Expirará en",
+        "expire_never": "Nunca expirará",
+        "forever": "Siempre",
         "hour": "Hora",
         "hourly": "Cada hora",
         "hours": "Horas",
@@ -1115,7 +1119,8 @@
         "shared_aliases": "Alias compartidos",
         "shared_aliases_desc": "Los alias compartidos no se ven afectados por la configuración específica del usuario, como el filtro de correo no deseado o la política de cifrado. Los filtros de spam correspondientes solo pueden ser realizados por un administrador como una política de dominio.",
         "sogo_profile_reset": "Resetear perfil SOGo",
-        "spam_aliases": "Alias de email temporales",
+        "spam_aliases": "Alias de email de spam",
+        "spam_aliases_info": "Un alias de spam es una dirección de correo electrónico temporal que se puede usar para proteger direcciones de correo electrónico reales. <br>Opcionalmente, se puede establecer un tiempo de expiración para que el alias se desactive automáticamente después del período definido, eliminando efectivamente las direcciones abusadas o filtradas.",
         "spamfilter": "Filtro anti-spam",
         "spamfilter_behavior": "Clasificación",
         "spamfilter_bl": "Lista negra",

+ 3 - 0
data/web/lang/lang.ja-jp.json

@@ -1187,6 +1187,7 @@
         "created_on": "作成日",
         "daily": "毎日",
         "day": "日",
+        "description": "説明",
         "delete_ays": "削除プロセスを確認してください。",
         "direct_aliases": "直接エイリアスアドレス",
         "direct_aliases_desc": "直接エイリアスアドレスは、スパムフィルターおよびTLSポリシー設定の影響を受けます。",
@@ -1201,7 +1202,9 @@
         "encryption": "暗号化",
         "excludes": "除外",
         "expire_in": "有効期限まで",
+        "expire_never": "有効期限なし",
         "fido2_webauthn": "FIDO2/WebAuthn",
+        "forever": "有効期限なし",
         "force_pw_update": "グループウェア関連サービスにアクセスするには、新しいパスワードを<b>必ず</b>設定する必要があります。",
         "from": "送信元",
         "generate": "生成",

+ 8 - 6
data/web/templates/user/SpamAliases.twig

@@ -8,6 +8,7 @@
     </div>
     <div id="collapse-tab-SpamAliases" class="card-body collapse" data-bs-parent="#user-content">
       <div class="row">
+        <p>{{ lang.user.spam_aliases_info|raw }}</p>
         <div class="col-md-12 col-sm-12 col-12">
           <table id="tla_table" class="table table-striped dt-responsive w-100"></table>
         </div>
@@ -18,12 +19,13 @@
             <a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="tla" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
             <a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
             <ul class="dropdown-menu">
-              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"1"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.hour }}</a></li>
-              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"24"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.day }}</a></li>
-              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"168"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.week }}</a></li>
-              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"744"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.month }}</a></li>
-              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"8760"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.year }}</a></li>
-              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"87600"}' href="#">{{ lang.user.expire_in }} 10 {{ lang.user.years }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"1","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.hour }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"24","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.day }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"168","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.week }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"744","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.month }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"8760","permanent":"0"}' href="#">{{ lang.user.expire_in }} 1 {{ lang.user.year }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"validity":"87600","permanent":"0"}' href="#">{{ lang.user.expire_in }} 10 {{ lang.user.years }}</a></li>
+              <li><a class="dropdown-item" data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{"permanent":"1"}' href="#">{{ lang.user.expire_never }}</a></li>
               <li><hr class="dropdown-divider"></li>
               <li><a class="dropdown-item" data-action="delete_selected" data-id="tla" data-api-url='delete/time_limited_alias' href="#">{{ lang.mailbox.remove }}</a></li>
             </ul>

+ 2 - 2
docker-compose.yml

@@ -117,7 +117,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: ghcr.io/mailcow/phpfpm:1.94
+      image: ghcr.io/mailcow/phpfpm:8.2.29
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
         - redis-mailcow
@@ -339,7 +339,7 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: ghcr.io/mailcow/postfix:1.81
+      image: ghcr.io/mailcow/postfix:3.7.11
       depends_on:
         mysql-mailcow:
           condition: service_started