NobGit
public nobgit read

NobMail

Based on mailcow: dockerized

Languages

Repository composition by tracked source files.

PHP
PHP 49% JavaScript 35% HTML 9% CSS 4% Shell 2% Python 1% Lua 0% Perl 0% Ruby 0% SCSS 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git

Commit

[Postfix] Configurable send permissions for alias addresses

c06112b2
FreddleSpl0it <[email protected]> 6 months ago
data/Dockerfiles/postfix/postfix.sh    |  3 +++
 data/web/inc/functions.mailbox.inc.php | 45 +++++++++++++++++++++++++++-------
 data/web/inc/init_db.inc.php           |  3 ++-
 data/web/lang/lang.de-de.json          |  3 +++
 data/web/lang/lang.en-gb.json          |  3 +++
 data/web/templates/edit/alias.twig     |  7 +++++-
 data/web/templates/edit/mailbox.twig   | 24 +++++++++++-------
 data/web/templates/modals/mailbox.twig |  7 +++++-
 docker-compose.yml                     |  2 +-
 9 files changed, 75 insertions(+), 22 deletions(-)

Diff

diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh
index 0a8ed736..51927ea1 100755
--- a/data/Dockerfiles/postfix/postfix.sh
+++ b/data/Dockerfiles/postfix/postfix.sh
@@ -329,14 +329,17 @@ query = SELECT goto FROM alias
           SELECT id FROM alias
             WHERE address='%s'
             AND (active='1' OR active='2')
+            AND sender_allowed='1'
         ), (
           SELECT id FROM alias
             WHERE address='@%d'
             AND (active='1' OR active='2')
+            AND sender_allowed='1'
         )
       )
     )
     AND active='1'
+    AND sender_allowed='1'
     AND (domain IN
       (SELECT domain FROM domain
         WHERE domain='%d'
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index d8e4e178..a4147ee9 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -695,6 +695,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $gotos           = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
           $internal        = intval($_data['internal']);
           $active          = intval($_data['active']);
+          $sender_allowed  = intval($_data['sender_allowed']);
           $sogo_visible    = intval($_data['sogo_visible']);
           $goto_null       = intval($_data['goto_null']);
           $goto_spam       = intval($_data['goto_spam']);
@@ -850,8 +851,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
-              VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
+            $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`)
+              VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :active)");
             if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
               $stmt->execute(array(
                 ':address' => '@'.$domain,
@@ -862,6 +863,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ':domain' => $domain,
                 ':sogo_visible' => $sogo_visible,
                 ':internal' => $internal,
+                ':sender_allowed' => $sender_allowed,
                 ':active' => $active
               ));
             }
@@ -874,6 +876,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ':domain' => $domain,
                 ':sogo_visible' => $sogo_visible,
                 ':internal' => $internal,
+                ':sender_allowed' => $sender_allowed,
                 ':active' => $active
               ));
             }
@@ -2501,6 +2504,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             if (!empty($is_now)) {
               $internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
               $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
+              $sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
               $sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
               $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
               $goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
@@ -2686,6 +2690,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 `goto` = :goto,
                 `sogo_visible`= :sogo_visible,
                 `internal`= :internal,
+                `sender_allowed`= :sender_allowed,
                 `active`= :active
                   WHERE `id` = :id");
               $stmt->execute(array(
@@ -2696,6 +2701,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ':goto' => $goto,
                 ':sogo_visible' => $sogo_visible,
                 ':internal' => $internal,
+                ':sender_allowed' => $sender_allowed,
                 ':active' => $active,
                 ':id' => $is_now['id']
               ));
@@ -3185,9 +3191,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
             if (isset($_data['sender_acl'])) {
               // Get sender_acl items set by admin
+              $current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
               $sender_acl_admin = array_merge(
-                mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
-                mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
+                $current_sender_acls['sender_acl_domains']['ro'],
+                $current_sender_acls['sender_acl_addresses']['ro']
               );
               // Get sender_acl items from POST array
               // Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
@@ -3275,16 +3282,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $stmt->execute(array(
                   ':username' => $username
                 ));
-                $fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
+                $sender_acl_handles = mailbox('get', 'sender_acl_handles', $username);
+                $fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
+                $fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
+
                 foreach ($sender_acl_merged as $sender_acl) {
                   $domain = ltrim($sender_acl, '@');
                   if (is_valid_domain_name($domain)) {
                     $sender_acl = '@' . $domain;
                   }
-                  // Don't add if allowed by alias
-                  if (in_array($sender_acl, $fixed_sender_aliases)) {
+
+                  // Always add to sender_acl table to create explicit permission
+                  // Skip only if it's in allowed list (would be redundant)
+                  // But DO add if it's in blocked list (creates override)
+                  if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
+                    // Skip: already allowed by sender_allowed=1, no need for sender_acl entry
                     continue;
                   }
+
+                  // Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
                   $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
                     VALUES (:sender_acl, :username)");
                   $stmt->execute(array(
@@ -4160,13 +4176,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $data['sender_acl_addresses']['rw']             = array();
           $data['sender_acl_addresses']['selectable']     = array();
           $data['fixed_sender_aliases']                   = array();
+          $data['fixed_sender_aliases_allowed']           = array();
+          $data['fixed_sender_aliases_blocked']           = array();
           $data['external_sender_aliases']                = array();
-          // Fixed addresses
-          $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
+          // Fixed addresses - split by sender_allowed status
+          $stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
           $stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
           $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           while ($row = array_shift($rows)) {
+            // Keep old array for backward compatibility
             $data['fixed_sender_aliases'][] = $row['address'];
+            // Split into allowed/blocked for proper display
+            if ($row['sender_allowed'] == '1') {
+              $data['fixed_sender_aliases_allowed'][] = $row['address'];
+            } else {
+              $data['fixed_sender_aliases_blocked'][] = $row['address'];
+            }
           }
           $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
             WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
@@ -4726,6 +4751,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             `internal`,
             `active`,
             `sogo_visible`,
+            `sender_allowed`,
             `created`,
             `modified`
               FROM `alias`
@@ -4759,6 +4785,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $aliasdata['active_int'] = $row['active'];
           $aliasdata['sogo_visible'] = $row['sogo_visible'];
           $aliasdata['sogo_visible_int'] = $row['sogo_visible'];
+          $aliasdata['sender_allowed'] = $row['sender_allowed'];
           $aliasdata['created'] = $row['created'];
           $aliasdata['modified'] = $row['modified'];
           if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index ffaf1209..37589bac 100644
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@ -4,7 +4,7 @@ function init_db_schema()
   try {
     global $pdo;
 
-    $db_version = "10312025_0525";
+    $db_version = "16122025_1230";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -185,6 +185,7 @@ function init_db_schema()
           "public_comment" => "TEXT",
           "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
         ),
         "keys" => array(
diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json
index 762c055a..b86879de 100644
--- a/data/web/lang/lang.de-de.json
+++ b/data/web/lang/lang.de-de.json
@@ -73,6 +73,7 @@
         "inactive": "Inaktiv",
         "internal": "Intern",
         "internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
+        "sender_allowed": "Als dieser Alias senden erlauben",
         "kind": "Art",
         "mailbox_quota_def": "Standard-Quota einer Mailbox",
         "mailbox_quota_m": "Max. Speicherplatz pro Mailbox (MiB)",
@@ -694,6 +695,8 @@
         "inactive": "Inaktiv",
         "internal": "Intern",
         "internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
+        "sender_allowed": "Als dieser Alias senden erlauben",
+        "sender_allowed_info": "Wenn deaktiviert, kann dieser Alias nur E-Mails empfangen. Verwenden Sie Sender-ACL, um bestimmten Postfächern die Berechtigung zum Senden zu erteilen.",
         "kind": "Art",
         "last_modified": "Zuletzt geändert",
         "lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json
index 1e852595..79175e57 100644
--- a/data/web/lang/lang.en-gb.json
+++ b/data/web/lang/lang.en-gb.json
@@ -73,6 +73,7 @@
         "inactive": "Inactive",
         "internal": "Internal",
         "internal_info": "Internal aliases are only accessible from the own domain or alias domains.",
+        "sender_allowed": "Allow to send as this alias",
         "kind": "Kind",
         "mailbox_quota_def": "Default mailbox quota",
         "mailbox_quota_m": "Max. quota per mailbox (MiB)",
@@ -694,6 +695,8 @@
         "inactive": "Inactive",
         "internal": "Internal",
         "internal_info": "Internal aliases are only accessible from the own domain or alias domains.",
+        "sender_allowed": "Allow to send as this alias",
+        "sender_allowed_info": "If disabled, this alias can only receive mail. Use sender ACL to override and grant specific mailboxes permission to send.",
         "kind": "Kind",
         "last_modified": "Last modified",
         "lookup_mx": "Destination is a regular expression to match against MX name (<code>.*\\.google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
diff --git a/data/web/templates/edit/alias.twig b/data/web/templates/edit/alias.twig
index 64a6e706..85a946ba 100644
--- a/data/web/templates/edit/alias.twig
+++ b/data/web/templates/edit/alias.twig
@@ -7,6 +7,7 @@
 <form class="form-horizontal" data-id="editalias" role="form" method="post">
   <input type="hidden" value="0" name="active">
   <input type="hidden" value="0" name="internal">
+  <input type="hidden" value="0" name="sender_allowed">
   {% if not skip_sogo %}
   <input type="hidden" value="0" name="sogo_visible">
   {% endif %}
@@ -39,7 +40,11 @@
       <div class="form-check">
         <label><input type="checkbox" class="form-check-input" value="1" name="internal"{% if result.internal == '1' %} checked{% endif %}> {{ lang.edit.internal }}</label>
       </div>
-      <small class="text-muted d-block">{{ lang.edit.internal_info }}</small>
+      <small class="text-muted d-block mb-2">{{ lang.edit.internal_info }}</small>
+      <div class="form-check">
+        <label><input type="checkbox" class="form-check-input" value="1" name="sender_allowed"{% if result.sender_allowed == '1' %} checked{% endif %}> {{ lang.edit.sender_allowed }}</label>
+      </div>
+      <small class="text-muted d-block">{{ lang.edit.sender_allowed_info }}</small>
     </div>
   </div>
   <hr>
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig
index a223a750..0b83dc5b 100644
--- a/data/web/templates/edit/mailbox.twig
+++ b/data/web/templates/edit/mailbox.twig
@@ -85,14 +85,6 @@
                             {{ lang.edit.dont_check_sender_acl|format(domain) }}
                           </option>
                         {% endfor %}
-                        {% for alias in sender_acl_handles.sender_acl_addresses.ro %}
-                          <option data-subtext="Admin" disabled selected>
-                            {{ alias }}
-                          </option>
-                        {% endfor %}
-                        {% for alias in sender_acl_handles.fixed_sender_aliases %}
-                          <option data-subtext="Alias" disabled selected>{{ alias }}</option>
-                        {% endfor %}
                         {% for domain in sender_acl_handles.sender_acl_domains.rw %}
                           <option value="{{ domain }}" selected>
                             {{ lang.edit.dont_check_sender_acl|format(domain) }}
@@ -104,11 +96,25 @@
                           </option>
                         {% endfor %}
                         {% for address in sender_acl_handles.sender_acl_addresses.rw %}
-                          <option selected>{{ address }}</option>
+                          {% if address in sender_acl_handles.fixed_sender_aliases_allowed or address in sender_acl_handles.fixed_sender_aliases_blocked %}
+                            <option data-subtext="Alias" selected>{{ address }}</option>
+                          {% else %}
+                            <option selected>{{ address }}</option>
+                          {% endif %}
                         {% endfor %}
                         {% for address in sender_acl_handles.sender_acl_addresses.selectable %}
                           <option>{{ address }}</option>
                         {% endfor %}
+                        {% for alias in sender_acl_handles.fixed_sender_aliases_allowed %}
+                          {% if alias not in sender_acl_handles.sender_acl_addresses.rw %}
+                            <option data-subtext="Alias (allowed)" value="{{ alias }}" selected>{{ alias }}</option>
+                          {% endif %}
+                        {% endfor %}
+                        {% for alias in sender_acl_handles.fixed_sender_aliases_blocked %}
+                          {% if alias not in sender_acl_handles.sender_acl_addresses.rw %}
+                            <option data-subtext="Alias (blocked)" value="{{ alias }}">{{ alias }}</option>
+                          {% endif %}
+                        {% endfor %}
                       </select>
                       <div id="sender_acl_disabled"><i class="bi bi-shield-exclamation"></i> {{ lang.edit.sender_acl_disabled|raw }}</div>
                       <small class="text-muted d-block">{{ lang.edit.sender_acl_info|raw }}</small>
diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig
index 1e8ee53f..2e0f589d 100644
--- a/data/web/templates/modals/mailbox.twig
+++ b/data/web/templates/modals/mailbox.twig
@@ -778,6 +778,7 @@
         <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias">
           <input type="hidden" value="0" name="active">
           <input type="hidden" value="0" name="internal">
+          <input type="hidden" value="0" name="sender_allowed">
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end" for="address">{{ lang.add.alias_address }}</label>
             <div class="col-sm-10">
@@ -809,7 +810,11 @@
               <div class="form-check">
                 <label><input type="checkbox" class="form-check-input" value="1" name="internal"> {{ lang.add.internal }}</label>
               </div>
-              <small class="text-muted d-block">{{ lang.edit.internal_info }}</small>
+              <small class="text-muted d-block mb-2">{{ lang.edit.internal_info }}</small>
+              <div class="form-check">
+                <label><input type="checkbox" class="form-check-input" value="1" name="sender_allowed" checked> {{ lang.add.sender_allowed }}</label>
+              </div>
+              <small class="text-muted d-block">{{ lang.edit.sender_allowed_info }}</small>
             </div>
           </div>
           <div class="row mb-4">
diff --git a/docker-compose.yml b/docker-compose.yml
index f09afca2..1fc28af6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -339,7 +339,7 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: ghcr.io/mailcow/postfix:3.7.11
+      image: ghcr.io/mailcow/postfix:3.7.11-1
       depends_on:
         mysql-mailcow:
           condition: service_started