1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-01-30 00:57:15 +00:00

[Postfix] Configurable send permissions for alias addresses

This commit is contained in:
FreddleSpl0it
2026-01-27 09:05:51 +01:00
parent 038b2efb75
commit c06112b26e
9 changed files with 75 additions and 22 deletions

View File

@@ -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'])) {

View File

@@ -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(

View File

@@ -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)",

View File

@@ -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)",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">