diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c152606d..0620404d 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -145,6 +145,16 @@ query = SELECT bcc_dest FROM bcc_maps AND active='1'; EOF +cat < /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT new_dest FROM recipient_maps + WHERE old_dest='%s' + AND active='1'; +EOF + cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf user = ${DBUSER} password = ${DBPASS} diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 4e8c577f..1c943dc6 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -45,6 +45,7 @@ proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf, $local_recipient_maps, $mydestination, $virtual_alias_maps, @@ -108,6 +109,8 @@ virtual_mailbox_base = /var/vmail/ virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf recipient_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf sender_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf +recipient_canonical_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf +recipient_canonical_classes = envelope_recipient virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf virtual_minimum_uid = 104 virtual_transport = lmtp:inet:dovecot:24 diff --git a/data/web/edit.php b/data/web/edit.php index b53e1794..5e337249 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -654,6 +654,43 @@ if (isset($_SESSION['mailcow_cc_role'])) { +

Recipient map:

+
+
+ +
+ +
+ + Recipient map destinations can only be valid email addresses. Separated by whitespace, semicolon, new line or comma. +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + 'danger', + 'msg' => 'Recipient map destination cannot be empty' + ); + return false; + } + if (is_valid_domain_name($old_dest)) { + $old_dest_sane = '@' . idn_to_ascii($old_dest); + } + elseif (filter_var($old_dest, FILTER_VALIDATE_EMAIL)) { + $old_dest_sane = $old_dest; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid original recipient specified: ' . $old_dest + ); + return false; + } + foreach ($new_dest as &$new_dest_e) { + if (!filter_var($new_dest_e, FILTER_VALIDATE_EMAIL)) { + $new_dest_e = null;; + } + $new_dest_e = strtolower($new_dest_e); + } + $new_dest = array_filter($new_dest); + $new_dest = implode(",", $new_dest); + if (empty($new_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Recipient map destination cannot be empty' + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `id` FROM `recipient_maps` + WHERE `old_dest` = :old_dest"); + $stmt->execute(array(':old_dest' => $old_dest_sane)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'A Recipient map entry "' . htmlspecialchars($old_dest_sane) . '" exists' + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `recipient_maps` (`old_dest`, `new_dest`, `active`) VALUES + (:old_dest, :new_dest, :active)"); + $stmt->execute(array( + ':old_dest' => $old_dest_sane, + ':new_dest' => $new_dest, + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Recipient map entry saved' + ); + break; + case 'edit': + $ids = (array)$_data['id']; + foreach ($ids as $id) { + $is_now = recipient_map('details', $id); + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $new_dest = (!empty($_data['recipient_map_new'])) ? $_data['recipient_map_new'] : $is_now['recipient_map_new']; + $old_dest = $is_now['old_dest']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $new_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $new_dest)); + $active = intval($_data['active']); + foreach ($new_dest as &$new_dest_e) { + if (!filter_var($new_dest_e, FILTER_VALIDATE_EMAIL)) { + $new_dest_e = null;; + } + $new_dest_e = strtolower($new_dest_e); + } + $new_dest = array_filter($new_dest); + $new_dest = implode(",", $new_dest); + if (empty($new_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Recipient map destination cannot be empty' + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `id` FROM `recipient_maps` + WHERE `old_dest` = :old_dest"); + $stmt->execute(array(':old_dest' => $old_dest)); + $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id']; + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if (isset($id_now) && $id_now != $id) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'A Recipient map entry ' . htmlspecialchars($old_dest) . ' exists' + ); + return false; + } + try { + $stmt = $pdo->prepare("UPDATE `recipient_maps` SET `new_dest` = :new_dest, `active` = :active WHERE `id`= :id"); + $stmt->execute(array( + ':new_dest' => $new_dest, + ':active' => $active, + ':id' => $id + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Recipient map entry edited' + ); + break; + case 'details': + $mapdata = array(); + $id = intval($_data); + try { + $stmt = $pdo->prepare("SELECT `id`, + `old_dest` AS `recipient_map_old`, + `new_dest` AS `recipient_map_new`, + `active` AS `active_int`, + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`, + `created`, + `modified` FROM `recipient_maps` + WHERE `id` = :id"); + $stmt->execute(array(':id' => $id)); + $mapdata = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + return $mapdata; + break; + case 'get': + $mapdata = array(); + $all_items = array(); + $id = intval($_data); + try { + $stmt = $pdo->query("SELECT `id` FROM `recipient_maps`"); + $all_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + foreach ($all_items as $i) { + $mapdata[] = $i['id']; + } + $all_items = null; + return $mapdata; + break; + case 'delete': + $ids = (array)$_data['id']; + foreach ($ids as $id) { + if (!is_numeric($id)) { + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `recipient_maps` WHERE `id`= :id"); + $stmt->execute(array(':id' => $id)); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Deleted Recipient map id/s ' . implode(', ', $ids) + ); + return true; + break; + } +} diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 6c0ef937..ef704bfb 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 = "02012018_1515"; + $db_version = "20012021_2202"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -394,6 +394,25 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "recipient_maps" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "old_dest" => "VARCHAR(255) NOT NULL", + "new_dest" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "local_dest" => array("old_dest"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "tfa" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 8aa20770..c7a75fdc 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -81,7 +81,7 @@ include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'. require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php'; -require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.bcc.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.address_rewriting.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantaine.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 1e08a4ee..cca7331b 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -383,6 +383,52 @@ jQuery(function($){ } }); } + function draw_recipient_map_table() { + ft_recipient_map_table = FooTable.init('#recipient_map_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"recipient_map_old","title":lang.recipient_map_old}, + {"name":"recipient_map_new","title":lang.recipient_map_new}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/recipient_map/all', + jsonp: false, + error: function () { + console.log('Cannot draw recipient map table'); + }, + success: function (data) { + if (role == "admin") { + $.each(data, function (i, item) { + item.action = ''; + item.chkbox = ''; + }); + } + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "filtering": { + "enabled": true, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + } + }); + } function draw_alias_table() { ft_alias_table = FooTable.init('#alias_table', { "columns": [ @@ -609,5 +655,6 @@ jQuery(function($){ draw_sync_job_table(); draw_filter_table(); draw_bcc_table(); + draw_recipient_map_table(); }); diff --git a/data/web/json_api.php b/data/web/json_api.php index 856facfc..8c2c5b44 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -595,6 +595,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "recipient_map": + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (recipient_map('add', $attr) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Cannot add item' + )); + } + } + else { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Cannot find attributes in post data' + )); + } + break; } break; case "get": @@ -1191,6 +1224,41 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u break; } break; + case "recipient_map": + switch ($object) { + case "all": + $recipient_map_items = recipient_map('get'); + if (!empty($recipient_map_items)) { + foreach ($recipient_map_items as $recipient_map_item) { + if ($details = recipient_map('details', $recipient_map_item)) { + $data[] = $details; + } + else { + continue; + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + default: + $data = recipient_map('details', $object); + if (!empty($data)) { + $data[] = $details; + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } + break; case "policy_wl_mailbox": switch ($object) { default: @@ -1739,6 +1807,47 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "recipient_map": + if (isset($_POST['items'])) { + $items = (array)json_decode($_POST['items'], true); + if (is_array($items)) { + if (recipient_map('delete', array('id' => $items)) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Deletion of items/s failed' + )); + } + } + else { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Cannot find id array in post data' + )); + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Cannot find items in post data' + )); + } + break; case "fwdhost": if (isset($_POST['items'])) { $items = (array)json_decode($_POST['items'], true); @@ -2238,6 +2347,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "recipient_map": + if (isset($_POST['items']) && isset($_POST['attr'])) { + $items = (array)json_decode($_POST['items'], true); + $attr = (array)json_decode($_POST['attr'], true); + $postarray = array_merge(array('id' => $items), $attr); + if (is_array($postarray['id'])) { + if (recipient_map('edit', $postarray) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Edit failed' + )); + } + exit(); + } + else { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } + break; case "alias": if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 37cf6661..2b3236fe 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -648,5 +648,11 @@ $lang['mailbox']['bcc_maps'] = "BCC maps"; $lang['mailbox']['bcc_to_sender'] = "Switch to sender map type"; $lang['mailbox']['bcc_to_rcpt'] = "Switch to recipient map type"; $lang['mailbox']['add_bcc_entry'] = "Add BCC map"; -$lang['mailbox']['bcc_info'] = "A recipient map type entry is used, when the local destination acts as recipient of a mail. Sender maps conform to the same principle.
+$lang['mailbox']['bcc_info'] = "BCC maps are used to silently forward copies of all messages to another address. A recipient map type entry is used, when the local destination acts as recipient of a mail. Sender maps conform to the same principle.
The local destination will not be informed about a failed delivery."; +$lang['mailbox']['address_rewriting'] = 'Address rewriting'; +$lang['mailbox']['recipient_maps'] = 'Recipient maps'; +$lang['mailbox']['recipient_map_info'] = 'Recipient maps are used to replace the destination address on a message before it is delivered.'; +$lang['mailbox']['recipient_map_old'] = 'Original recipient'; +$lang['mailbox']['recipient_map_new'] = 'New recipient'; +$lang['mailbox']['add_recipient_map_entry'] = 'Add recipient map'; \ No newline at end of file diff --git a/data/web/mailbox.php b/data/web/mailbox.php index 19ec683e..73a628c4 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -21,7 +21,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
  • -
  • +
  • @@ -234,6 +234,34 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
    +
    +
    +

    +
    +

    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php index 3f05f8f8..f8dca77d 100644 --- a/data/web/modals/mailbox.php +++ b/data/web/modals/mailbox.php @@ -574,6 +574,45 @@ if (!isset($_SESSION['mailcow_cc_role'])) { + +