Browse Source

[Web] Feature: TLS policy maps
[Web] Avoid php extensions in links
[Web] Minor fixes

André 7 years ago
parent
commit
f5799faf6d

+ 64 - 1
data/web/edit.php

@@ -731,7 +731,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         <?php
         }
     }
-    elseif (isset($_GET['recipient_map']) && !empty($_GET["recipient_map"])) {
+    elseif (isset($_GET['recipient_map']) &&
+      !empty($_GET["recipient_map"]) &&
+      $_SESSION['mailcow_cc_role'] == "admin") {
         $map = intval($_GET["recipient_map"]);
         $result = recipient_map('details', $map);
         if (substr($result['recipient_map_old'], 0, 1) == '@') {
@@ -778,6 +780,67 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         <?php
         }
     }
+    elseif (isset($_GET['tls_policy_map']) &&
+      !empty($_GET["tls_policy_map"]) &&
+      $_SESSION['mailcow_cc_role'] == "admin") {
+        $map = intval($_GET["tls_policy_map"]);
+        $result = tls_policy_maps('details', $map);
+        if (!empty($result)) {
+          ?>
+          <h4><?=$lang['mailbox']['tls_policy_maps']?>: <?=$result['dest'];?></h4>
+          <br />
+          <form class="form-horizontal" data-id="edit_tls_policy_maps" role="form" method="post">
+            <input type="hidden" value="0" name="active">
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="dest"><?=$lang['mailbox']['tls_map_dest'];?></label>
+              <div class="col-sm-10">
+                <input value="<?=$result['dest'];?>" type="text" class="form-control" name="dest" id="dest">
+                <small><?=$lang['mailbox']['tls_map_dest_info'];?></small>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="policy"><?=$lang['mailbox']['tls_map_policy'];?></label>
+              <div class="col-sm-10">
+              <select class="full-width-select" name="policy" required>
+                <option value="none" <?=($result['policy'] != 'none') ?: 'selected';?>>none</option>
+                <option value="may" <?=($result['policy'] != 'may') ?: 'selected';?>>may</option>
+                <option value="encrypt" <?=($result['policy'] != 'encrypt') ?: 'selected';?>>encrypt</option>
+                <option value="dane" <?=($result['policy'] != 'dane') ?: 'selected';?>>dane-only</option>
+                <option value="dane-only" <?=($result['policy'] != 'dane-only') ?: 'selected';?>>dane-only</option>
+                <option value="fingerprint" <?=($result['policy'] != 'fingerprint') ?: 'selected';?>>fingerprint</option>
+                <option value="verify" <?=($result['policy'] != 'verify') ?: 'selected';?>>verify</option>
+                <option value="secure" <?=($result['policy'] != 'secure') ?: 'selected';?>>secure</option>
+              </select>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="parameters"><?=$lang['mailbox']['tls_map_parameters'];?></label>
+              <div class="col-sm-10">
+                <input value="<?=$result['parameters'];?>" type="text" class="form-control" name="parameters" id="parameters">
+                <small><?=$lang['mailbox']['tls_map_parameters_info'];?></small>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <div class="checkbox">
+                <label><input type="checkbox" value="1" name="active" <?php if (isset($result['active_int']) && $result['active_int']=="1") { echo "checked"; }; ?>> <?=$lang['edit']['active'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <button class="btn btn-success" data-action="edit_selected" data-id="edit_tls_policy_maps" data-item="<?=$map;?>" data-api-url='edit/tls-policy-map' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+              </div>
+            </div>
+          </form>
+        <?php
+        }
+        else {
+        ?>
+          <div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+        <?php
+        }
+    }
   }
   if ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin" || $_SESSION['mailcow_cc_role'] == "user") {
     if (isset($_GET['syncjob']) &&

+ 68 - 130
data/web/inc/functions.address_rewriting.inc.php

@@ -87,25 +87,15 @@ function bcc($_action, $_data = null, $attr = null) {
         );
         return false;
       }
-      try {
-        $stmt = $pdo->prepare("INSERT INTO `bcc_maps` (`local_dest`, `bcc_dest`, `domain`, `active`, `type`) VALUES
-          (:local_dest, :bcc_dest, :domain, :active, :type)");
-        $stmt->execute(array(
-          ':local_dest' => $local_dest_sane,
-          ':bcc_dest' => $bcc_dest,
-          ':domain' => $domain,
-          ':active' => $active,
-          ':type' => $type
-        ));
-      }
-      catch (PDOException $e) {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => array('mysql_error', $e)
-        );
-        return false;
-      }
+      $stmt = $pdo->prepare("INSERT INTO `bcc_maps` (`local_dest`, `bcc_dest`, `domain`, `active`, `type`) VALUES
+        (:local_dest, :bcc_dest, :domain, :active, :type)");
+      $stmt->execute(array(
+        ':local_dest' => $local_dest_sane,
+        ':bcc_dest' => $bcc_dest,
+        ':domain' => $domain,
+        ':active' => $active,
+        ':type' => $type
+      ));
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -155,37 +145,27 @@ function bcc($_action, $_data = null, $attr = null) {
           );
           continue;
         }
-        try {
-          $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
-            WHERE `local_dest` = :local_dest AND `type` = :type");
-          $stmt->execute(array(':local_dest' => $local_dest, ':type' => $type));
-          $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
+        $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
+          WHERE `local_dest` = :local_dest AND `type` = :type");
+        $stmt->execute(array(':local_dest' => $local_dest, ':type' => $type));
+        $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
 
-          if (isset($id_now) && $id_now != $id) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-              'msg' => array('bcc_exists', htmlspecialchars($local_dest), $type)
-            );
-            continue;
-          }
-
-          $stmt = $pdo->prepare("UPDATE `bcc_maps` SET `bcc_dest` = :bcc_dest, `active` = :active, `type` = :type WHERE `id`= :id");
-          $stmt->execute(array(
-            ':bcc_dest' => $bcc_dest,
-            ':active' => $active,
-            ':type' => $type,
-            ':id' => $id
-          ));
-        }
-        catch (PDOException $e) {
+        if (isset($id_now) && $id_now != $id) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
+            'msg' => array('bcc_exists', htmlspecialchars($local_dest), $type)
           );
           continue;
         }
+
+        $stmt = $pdo->prepare("UPDATE `bcc_maps` SET `bcc_dest` = :bcc_dest, `active` = :active, `type` = :type WHERE `id`= :id");
+        $stmt->execute(array(
+          ':bcc_dest' => $bcc_dest,
+          ':active' => $active,
+          ':type' => $type,
+          ':id' => $id
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -246,29 +226,20 @@ function bcc($_action, $_data = null, $attr = null) {
         if (!is_numeric($id)) {
           return false;
         }
-        try {
-          $stmt = $pdo->prepare("SELECT `domain` FROM `bcc_maps` WHERE id = :id");
-          $stmt->execute(array(':id' => $id));
-          $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain'];
-          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-              'msg' => 'access_denied'
-            );
-            continue;
-          }
-          $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `id`= :id");
-          $stmt->execute(array(':id' => $id));
-        }
-        catch (PDOException $e) {
+        $stmt = $pdo->prepare("SELECT `domain` FROM `bcc_maps` WHERE id = :id");
+        $stmt->execute(array(':id' => $id));
+        $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain'];
+        if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
+            'msg' => 'access_denied'
           );
           continue;
         }
+        $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `id`= :id");
+        $stmt->execute(array(':id' => $id));
+
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -317,33 +288,22 @@ function recipient_map($_action, $_data = null, $attr = null) {
       }
       $rmaps = recipient_map('get');
       foreach ($rmaps as $rmap) {
-        $old_dests_existing[] = recipient_map('details', $rmap)['recipient_map_old'];
-      }
-      if (in_array($old_dest_sane, $old_dests_existing)) {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest))
-        );
-        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',
-          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => array('mysql_error', $e)
-        );
-        return false;
+        if (recipient_map('details', $rmap)['recipient_map_old'] == $old_dest_sane) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+            'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest_sane))
+          );
+          return false;
+        }
       }
+      $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
+      ));
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -384,7 +344,6 @@ function recipient_map($_action, $_data = null, $attr = null) {
           );
           continue;
         }
-        $active = intval($_data['active']);
         if (!filter_var($new_dest, FILTER_VALIDATE_EMAIL)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
@@ -395,38 +354,27 @@ function recipient_map($_action, $_data = null, $attr = null) {
         }
         $rmaps = recipient_map('get');
         foreach ($rmaps as $rmap) {
-          $old_dests_existing[] = recipient_map('details', $rmap)['recipient_map_old'];
-        }
-        if (in_array($old_dest_sane, $old_dests_existing) &&
-          recipient_map('details', $id)['recipient_map_old'] != $old_dest_sane) {
+          if ($rmap == $id) { continue; }
+          if (recipient_map('details', $rmap)['recipient_map_old'] == $old_dest_sane) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_data, $_attr),
               'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest_sane))
             );
-            continue;
-        }
-        try {
-          $stmt = $pdo->prepare("UPDATE `recipient_maps` SET
-            `old_dest` = :old_dest,
-            `new_dest` = :new_dest,
-            `active` = :active
-              WHERE `id`= :id");
-          $stmt->execute(array(
-            ':old_dest' => $old_dest_sane,
-            ':new_dest' => $new_dest,
-            ':active' => $active,
-            ':id' => $id
-          ));
-        }
-        catch (PDOException $e) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
-          );
-          return false;
+            return false;
+          }
         }
+        $stmt = $pdo->prepare("UPDATE `recipient_maps` SET
+          `old_dest` = :old_dest,
+          `new_dest` = :new_dest,
+          `active` = :active
+            WHERE `id`= :id");
+        $stmt->execute(array(
+          ':old_dest' => $old_dest_sane,
+          ':new_dest' => $new_dest,
+          ':active' => $active,
+          ':id' => $id
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -471,24 +419,14 @@ function recipient_map($_action, $_data = null, $attr = null) {
         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',
-            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
-          );
-          return false;
-        }
+        $stmt = $pdo->prepare("DELETE FROM `recipient_maps` WHERE `id`= :id");
+        $stmt->execute(array(':id' => $id));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => array('recipient_map_entry_deleted', htmlspecialchars($id))
+        );
       }
-      $_SESSION['return'][] = array(
-        'type' => 'success',
-        'msg' => array('recipient_map_entry_deleted', htmlspecialchars($old_dest))
-      );
-      return true;
     break;
   }
 }

+ 157 - 0
data/web/inc/functions.tls_policy_maps.inc.php

@@ -0,0 +1,157 @@
+<?php
+function tls_policy_maps($_action, $_data = null, $attr = null) {
+	global $pdo;
+	global $lang;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    return false;
+  }
+  switch ($_action) {
+    case 'add':
+      $dest = idn_to_ascii(trim($_data['dest']));
+      $policy = strtolower(trim($_data['policy']));
+      $parameters = (isset($_data['parameters']) && !empty($_data['parameters'])) ? $_data['parameters'] : '';
+      if (!empty($parameters)) {
+        foreach (explode(' ', $parameters) as $parameter) {
+          if (!preg_match('/(.+)\=(.+)/i', $parameter)) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+              'msg' => 'tls_policy_map_parameter_invalid'
+            );
+            return false;
+          }
+        }
+      }
+      $active = intval($_data['active']);
+      $tls_policy_maps = tls_policy_maps('get');
+      foreach ($tls_policy_maps as $tls_policy_map) {
+        if (tls_policy_maps('details', $tls_policy_map)['dest'] == $dest) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+            'msg' => array('tls_policy_map_entry_exists', htmlspecialchars($dest))
+          );
+          return false;
+        }
+      }
+      $stmt = $pdo->prepare("INSERT INTO `tls_policy_override` (`dest`, `policy`, `parameters`, `active`) VALUES
+        (:dest, :policy, :parameters, :active)");
+      $stmt->execute(array(
+        ':dest' => $dest,
+        ':policy' => $policy,
+        ':parameters' => $parameters,
+        ':active' => $active
+      ));
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+        'msg' => array('tls_policy_map_entry_saved', htmlspecialchars($dest))
+      );
+    break;
+    case 'edit':
+      $ids = (array)$_data['id'];
+      foreach ($ids as $id) {
+        $is_now = tls_policy_maps('details', $id);
+        if (!empty($is_now)) {
+          $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+          $dest = (!empty($_data['dest'])) ? $_data['dest'] : $is_now['dest'];
+          $policy = (!empty($_data['policy'])) ? $_data['policy'] : $is_now['policy'];
+          $parameters = (isset($_data['parameters'])) ? $_data['parameters'] : $is_now['parameters'];
+        }
+        else {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+            'msg' => 'access_denied'
+          );
+          continue;
+        }
+        if (!empty($parameters)) {
+          foreach (explode(' ', $parameters) as $parameter) {
+            if (!preg_match('/(.+)\=(.+)/i', $parameter)) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+                'msg' => 'tls_policy_map_parameter_invalid'
+              );
+              return false;
+            }
+          }
+        }
+        $tls_policy_maps = tls_policy_maps('get');
+        foreach ($tls_policy_maps as $tls_policy_map) {
+          if ($tls_policy_map == $id) { continue; }
+          if (tls_policy_maps('details', $tls_policy_map)['dest'] == $dest) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+              'msg' => array('recipient_map_entry_exists', htmlspecialchars($dest))
+            );
+            return false;
+          }
+        }
+        $stmt = $pdo->prepare("UPDATE `tls_policy_override` SET
+          `dest` = :dest,
+          `policy` = :policy,
+          `parameters` = :parameters,
+          `active` = :active
+            WHERE `id`= :id");
+        $stmt->execute(array(
+          ':dest' => $dest,
+          ':policy' => $policy,
+          ':parameters' => $parameters,
+          ':active' => $active,
+          ':id' => $id
+        ));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => array('tls_policy_map_entry_saved', htmlspecialchars($dest))
+        );
+      }
+    break;
+    case 'details':
+      $mapdata = array();
+      $id = intval($_data);
+      $stmt = $pdo->prepare("SELECT `id`,
+        `dest`,
+        `policy`,
+        `parameters`,
+        `active` AS `active_int`,
+        CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+        `created`,
+        `modified` FROM `tls_policy_override`
+          WHERE `id` = :id");
+      $stmt->execute(array(':id' => $id));
+      $mapdata = $stmt->fetch(PDO::FETCH_ASSOC);
+      return $mapdata;
+    break;
+    case 'get':
+      $mapdata = array();
+      $all_items = array();
+      $id = intval($_data);
+      $stmt = $pdo->query("SELECT `id` FROM `tls_policy_override`");
+      $all_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      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;
+        }
+        $stmt = $pdo->prepare("DELETE FROM `tls_policy_override` WHERE `id`= :id");
+        $stmt->execute(array(':id' => $id));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => array('tls_policy_map_entry_deleted', htmlspecialchars($id))
+        );
+      }
+    break;
+  }
+}

+ 125 - 125
data/web/inc/header.inc.php

@@ -1,145 +1,145 @@
 <!DOCTYPE html>
 <html lang="<?= $_SESSION['mailcow_locale'] ?>">
 <head>
-<meta charset="utf-8">
-<meta http-equiv="X-UA-Compatible" content="IE=edge">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<meta name="theme-color" content="#F5D76E"/>
-<meta http-equiv="Referrer-Policy" content="same-origin">
-<title><?=$UI_TEXTS['title_name'];?></title>
-<!--[if lt IE 9]>
-  <script src="/js/html5shiv.min.js"></script>
-  <script src="/js/respond.min.js"></script>
-<![endif]-->
-<script src="/js/jquery-1.12.4.min.js"></script>
-<?php if (strtolower(trim($DEFAULT_THEME)) != "lumen"): ?>
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/<?= strtolower(trim($DEFAULT_THEME)); ?>/bootstrap.min.css">
-<?php else: ?>
-<link rel="stylesheet" href="/css/bootstrap.min.css">
-<?php endif; ?>
-<link rel="stylesheet" href="/css/breakpoint.min.css">
-<link rel="stylesheet" href="/css/bootstrap-select.min.css">
-<link rel="stylesheet" href="/css/bootstrap-slider.min.css">
-<link rel="stylesheet" href="/css/bootstrap-switch.min.css">
-<link rel="stylesheet" href="/css/footable.bootstrap.min.css">
-<link rel="stylesheet" href="/inc/languages.min.css">
-<link rel="stylesheet" href="/css/mailcow.css">
-<link rel="stylesheet" href="/css/animate.min.css">
-<link rel="stylesheet" href="/css/numberedtextarea.min.css">
-<link rel="stylesheet" href="/css/jquery.jqplot.min.css">
-<?= (preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null; ?>
-<?= (preg_match("/admin.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null; ?>
-<?= (preg_match("/user.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/user.css">' : null; ?>
-<?= (preg_match("/edit.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/edit.css">' : null; ?>
-<?= (preg_match("/quarantine.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/quarantine.css">' : null; ?>
-<?= (preg_match("/debug.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/debug.css">' : null; ?>
-<link rel="shortcut icon" href="/favicon.png" type="image/png">
-<link rel="icon" href="/favicon.png" type="image/png">
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="theme-color" content="#F5D76E"/>
+  <meta http-equiv="Referrer-Policy" content="same-origin">
+  <title><?=$UI_TEXTS['title_name'];?></title>
+  <!--[if lt IE 9]>
+    <script src="/js/html5shiv.min.js"></script>
+    <script src="/js/respond.min.js"></script>
+  <![endif]-->
+  <script src="/js/jquery-1.12.4.min.js"></script>
+  <?php if (strtolower(trim($DEFAULT_THEME)) != "lumen"): ?>
+  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/<?= strtolower(trim($DEFAULT_THEME)); ?>/bootstrap.min.css">
+  <?php else: ?>
+  <link rel="stylesheet" href="/css/bootstrap.min.css">
+  <?php endif; ?>
+  <link rel="stylesheet" href="/css/breakpoint.min.css">
+  <link rel="stylesheet" href="/css/bootstrap-select.min.css">
+  <link rel="stylesheet" href="/css/bootstrap-slider.min.css">
+  <link rel="stylesheet" href="/css/bootstrap-switch.min.css">
+  <link rel="stylesheet" href="/css/footable.bootstrap.min.css">
+  <link rel="stylesheet" href="/inc/languages.min.css">
+  <link rel="stylesheet" href="/css/mailcow.css">
+  <link rel="stylesheet" href="/css/animate.min.css">
+  <link rel="stylesheet" href="/css/numberedtextarea.min.css">
+  <link rel="stylesheet" href="/css/jquery.jqplot.min.css">
+  <?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null; ?>
+  <?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null; ?>
+  <?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/user.css">' : null; ?>
+  <?= (preg_match("/edit/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/edit.css">' : null; ?>
+  <?= (preg_match("/quarantine/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/quarantine.css">' : null; ?>
+  <?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/debug.css">' : null; ?>
+  <link rel="shortcut icon" href="/favicon.png" type="image/png">
+  <link rel="icon" href="/favicon.png" type="image/png">
 </head>
 <body id="top">
-<div class="overlay"></div>
-<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
-  <div class="container-fluid">
-    <div class="navbar-header">
-      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-        <span class="icon-bar"></span>
-        <span class="icon-bar"></span>
-        <span class="icon-bar"></span>
-      </button>
-      <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>"></a>
-    </div>
-    <div id="navbar" class="navbar-collapse collapse">
-      <ul class="nav navbar-nav navbar-right">
-        <?php
-        if (isset($_SESSION['mailcow_locale'])) {
-        ?>
-        <li class="dropdown<?=(isset($_SESSION['mailcow_locale']) && count($AVAILABLE_LANGUAGES) === 1) ? ' lang-link-disabled"' : '' ?>">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="lang-sm lang-lbl" lang="<?= $_SESSION['mailcow_locale']; ?>"></span><span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
-            <?php
-            foreach ($AVAILABLE_LANGUAGES as $language) {
-            ?>
-            <li<?= ($_SESSION['mailcow_locale'] == $language) ? ' class="active"' : ''; ?>><a href="?<?= http_build_query(array_merge($_GET, array('lang' => $language))); ?>"><span class="lang-xs lang-lbl-full" lang="<?= $language; ?>"></span></a></li>
-            <?php
-            }
-            ?>
-          </ul>
-        </li>
-        <?php
-        }
-        if (isset($_SESSION['mailcow_cc_role'])) {
-        ?>
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?= $lang['header']['mailcow_settings']; ?> <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
-            <?php
-            if (isset($_SESSION['mailcow_cc_role'])) {
-              if ($_SESSION['mailcow_cc_role'] == 'admin') {
-              ?>
-                <li<?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/admin.php"><?= $lang['header']['administration']; ?></a></li>
-                <li<?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/debug.php"><?= $lang['header']['debug']; ?></a></li>
+  <div class="overlay"></div>
+  <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
+    <div class="container-fluid">
+      <div class="navbar-header">
+        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+        </button>
+        <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>"></a>
+      </div>
+      <div id="navbar" class="navbar-collapse collapse">
+        <ul class="nav navbar-nav navbar-right">
+          <?php
+          if (isset($_SESSION['mailcow_locale'])) {
+          ?>
+          <li class="dropdown<?=(isset($_SESSION['mailcow_locale']) && count($AVAILABLE_LANGUAGES) === 1) ? ' lang-link-disabled"' : '' ?>">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="lang-sm lang-lbl" lang="<?= $_SESSION['mailcow_locale']; ?>"></span><span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
               <?php
-              }
-              if ($_SESSION['mailcow_cc_role'] == 'admin' || $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+              foreach ($AVAILABLE_LANGUAGES as $language) {
               ?>
-                <li<?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/mailbox.php"><?= $lang['header']['mailboxes']; ?></a></li>
+              <li<?= ($_SESSION['mailcow_locale'] == $language) ? ' class="active"' : ''; ?>><a href="?<?= http_build_query(array_merge($_GET, array('lang' => $language))); ?>"><span class="lang-xs lang-lbl-full" lang="<?= $language; ?>"></span></a></li>
               <?php
               }
-              if ($_SESSION['mailcow_cc_role'] != 'admin') {
               ?>
-                <li<?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/user.php"><?= $lang['header']['user_settings']; ?></a></li>
+            </ul>
+          </li>
+          <?php
+          }
+          if (isset($_SESSION['mailcow_cc_role'])) {
+          ?>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?= $lang['header']['mailcow_settings']; ?> <span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
               <?php
+              if (isset($_SESSION['mailcow_cc_role'])) {
+                if ($_SESSION['mailcow_cc_role'] == 'admin') {
+                ?>
+                  <li<?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/admin"><?= $lang['header']['administration']; ?></a></li>
+                  <li<?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/debug"><?= $lang['header']['debug']; ?></a></li>
+                <?php
+                }
+                if ($_SESSION['mailcow_cc_role'] == 'admin' || $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+                ?>
+                  <li<?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/mailbox"><?= $lang['header']['mailboxes']; ?></a></li>
+                <?php
+                }
+                if ($_SESSION['mailcow_cc_role'] != 'admin') {
+                ?>
+                  <li<?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/user"><?= $lang['header']['user_settings']; ?></a></li>
+                <?php
+                }
               }
-            }
-            ?>
-          </ul>
-        </li>
-        <?php
-        if (isset($_SESSION['mailcow_cc_role'])) {
-        ?>
-        <li<?= (preg_match("/quarantine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantine.php"><span class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantine']; ?></a></li>
-        <?php
-        }
-        if ($_SESSION['mailcow_cc_role'] == 'admin') {
-        ?>
-        <li><a href data-toggle="modal" data-container="sogo-mailcow" data-target="#RestartContainer"><span class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_sogo']; ?></a></li>
-        <?php
-        }
-        ?>
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="glyphicon glyphicon-link"></span> Apps <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
+              ?>
+            </ul>
+          </li>
           <?php
-          foreach ($MAILCOW_APPS as $app):
+          if (isset($_SESSION['mailcow_cc_role'])) {
           ?>
-            <li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li>
+          <li<?= (preg_match("/quarantine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantine"><span class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantine']; ?></a></li>
           <?php
-          endforeach;
-          $app_links = customize('get', 'app_links');
-          foreach ($app_links as $row) {
-            foreach ($row as $key => $val):
+          }
+          if ($_SESSION['mailcow_cc_role'] == 'admin') {
           ?>
-            <li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></a></li>
+          <li><a href data-toggle="modal" data-container="sogo-mailcow" data-target="#RestartContainer"><span class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_sogo']; ?></a></li>
           <?php
+          }
+          ?>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="glyphicon glyphicon-link"></span> Apps <span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
+            <?php
+            foreach ($MAILCOW_APPS as $app):
+            ?>
+              <li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li>
+            <?php
             endforeach;
+            $app_links = customize('get', 'app_links');
+            foreach ($app_links as $row) {
+              foreach ($row as $key => $val):
+            ?>
+              <li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></a></li>
+            <?php
+              endforeach;
+            }
+            ?>
+            </ul>
+          </li>
+          <?php
           }
+          if (!isset($_SESSION['dual-login']) && isset($_SESSION['mailcow_cc_username'])):
+          ?>
+            <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?></b> <span class="glyphicon glyphicon-log-out"></span></a></li>
+          <?php
+          elseif (isset($_SESSION['dual-login'])):
+          ?>
+            <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?> <span class="text-info">(<?= htmlspecialchars($_SESSION['dual-login']['username']); ?>)</span> </b><span class="glyphicon glyphicon-log-out"></span></a></li>
+          <?php
+          endif;
           ?>
-          </ul>
-        </li>
-        <?php
-        }
-        if (!isset($_SESSION['dual-login']) && isset($_SESSION['mailcow_cc_username'])):
-        ?>
-          <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?></b> <span class="glyphicon glyphicon-log-out"></span></a></li>
-        <?php
-        elseif (isset($_SESSION['dual-login'])):
-        ?>
-          <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?> <span class="text-info">(<?= htmlspecialchars($_SESSION['dual-login']['username']); ?>)</span> </b><span class="glyphicon glyphicon-log-out"></span></a></li>
-        <?php
-        endif;
-        ?>
-      </ul>
-    </div><!--/.nav-collapse -->
-  </div><!--/.container-fluid -->
-</nav>
-<form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
+        </ul>
+      </div><!--/.nav-collapse -->
+    </div><!--/.container-fluid -->
+  </nav>
+  <form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>

+ 21 - 1
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "21092018_1902";
+    $db_version = "03102018_1502";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -192,6 +192,26 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "tls_policy_override" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "dest" => "VARCHAR(255) NOT NULL",
+          "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL",
+          "parameters" => "VARCHAR(255) DEFAULT ''",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
+          "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("id")
+          ),
+          "unique" => array(
+            "dest" => array("dest")
+          ),
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "quarantine" => array(
         "cols" => array(
           "id" => "INT NOT NULL AUTO_INCREMENT",

+ 1 - 0
data/web/inc/prerequisites.inc.php

@@ -146,6 +146,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rsettings.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';

+ 3 - 3
data/web/index.php

@@ -2,15 +2,15 @@
 require_once 'inc/prerequisites.inc.php';
 
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
-  header('Location: /admin.php');
+  header('Location: /admin');
   exit();
 }
 elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
-  header('Location: /mailbox.php');
+  header('Location: /mailbox');
   exit();
 }
 elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
-  header('Location: /user.php');
+  header('Location: /user');
   exit();
 }
 require_once 'inc/header.inc.php';

+ 2 - 2
data/web/js/admin.js

@@ -121,7 +121,7 @@ jQuery(function($){
       $.each(data, function (i, item) {
         item.action = '<div class="btn-group">' +
           '<a href="#" data-toggle="modal" id="miau" data-target="#testRelayhostModal" data-relayhost-id="' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-stats"></span> Test</a>' +
-          '<a href="/edit.php?relayhost=' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="/edit/relayhost/' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
           '<a href="#" data-action="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
           '</div>';
         item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
@@ -144,7 +144,7 @@ jQuery(function($){
         item.selected_domains = escapeHtml(item.selected_domains.toString().replace(/,/g, " "));
         item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
         item.action = '<div class="btn-group">' +
-          '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
           '<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
           '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' +
           '</div>';

+ 80 - 11
data/web/js/mailbox.js

@@ -300,11 +300,11 @@ jQuery(function($){
             item.chkbox = '<input type="checkbox" data-id="domain" name="multi_select" value="' + encodeURIComponent(item.domain_name) + '" />';
             item.action = '<div class="btn-group">';
             if (role == "admin") {
-              item.action += '<a href="/edit.php?domain=' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
                 '<a href="#" data-action="delete_selected" data-id="single-domain" data-api-url="delete/domain" data-item="' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>';
             }
             else {
-              item.action += '<a href="/edit.php?domain=' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>';
+              item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>';
             }
             item.action += '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.domain_name) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>';
           });
@@ -353,6 +353,8 @@ jQuery(function($){
         },
         },
         {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"},
+        {"name":"tls_enforce_in","filterable": false,"title":lang.tls_enforce_in,"breakpoints":"all"},
+        {"name":"tls_enforce_out","filterable": false,"title":lang.tls_enforce_out,"breakpoints":"all"},
         {"name":"in_use","filterable": false,"type":"html","title":lang.in_use,"sortValue": function(value){
           return Number($(value).find(".progress-bar").attr('aria-valuenow'));
         },
@@ -382,16 +384,18 @@ jQuery(function($){
               }).join('/1');
             }
             item.chkbox = '<input type="checkbox" data-id="mailbox" name="multi_select" value="' + encodeURIComponent(item.username) + '" />';
+            item.tls_enforce_in = '<span class="text-' + (item.attributes.tls_enforce_in == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>';
+            item.tls_enforce_out = '<span class="text-' + (item.attributes.tls_enforce_out == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>';
             if (acl_data.login_as === 1) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?mailbox=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="login_as btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' +
               '</div>';
             }
             else {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?mailbox=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             }
@@ -412,6 +416,7 @@ jQuery(function($){
         "delay": 100,
         "position": "left",
         "connectors": false,
+        //"container": "#tab-mailboxes.panel",
         "placeholder": lang.filter_table
       },
       "components": {
@@ -459,7 +464,7 @@ jQuery(function($){
               item.multiple_bookings = '<span id="active-script" class="label label-danger">' + lang.booking_custom_short + ' (' + item.multiple_bookings + ')</span>';
             }
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?resource=' + encodeURIComponent(item.name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/resource/' + encodeURIComponent(item.name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-resource" data-api-url="delete/resource" data-item="' + item.name + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />';
@@ -518,7 +523,7 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?bcc=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/bcc/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-bcc" data-api-url="delete/bcc" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="bcc" name="multi_select" value="' + item.id + '" />';
@@ -581,7 +586,7 @@ jQuery(function($){
               item.recipient_map_old = escapeHtml(item.recipient_map_old);
               item.recipient_map_new = escapeHtml(item.recipient_map_new);
               item.action = '<div class="btn-group">' +
-                '<a href="/edit.php?recipient_map=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="/edit/recipient_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
                 '<a href="#" data-action="delete_selected" data-id="single-recipient_map" data-api-url="delete/recipient_map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
                 '</div>';
               item.chkbox = '<input type="checkbox" data-id="recipient_map" name="multi_select" value="' + item.id + '" />';
@@ -614,6 +619,69 @@ jQuery(function($){
       }
     });
   }
+  function draw_tls_policy_table() {
+    ft_tls_policy_table = FooTable.init('#tls_policy_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":"dest","title":lang.tls_map_dest},
+        {"name":"policy","title":lang.tls_map_policy},
+        {"name":"parameters","title":lang.tls_map_parameters},
+        {"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/tls-policy-map/all',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw tls policy map table');
+        },
+        success: function (data) {
+          if (role == "admin") {
+            $.each(data, function (i, item) {
+              item.dest = escapeHtml(item.dest);
+              item.policy = '<b>' + escapeHtml(item.policy) + '</b>';
+              if (item.parameters == '') {
+                item.parameters = '<code>-</code>';
+              } else {
+                item.parameters = '<code>' + escapeHtml(item.parameters) + '</code>';
+              }
+              item.action = '<div class="btn-group">' +
+                '<a href="/edit/tls_policy_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-tls-policy-map" data-api-url="delete/tls-policy-map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+                '</div>';
+              item.chkbox = '<input type="checkbox" data-id="tls-policy-map" name="multi_select" value="' + item.id + '" />';
+            });
+          }
+        }
+      }),
+      "paging": {
+        "enabled": true,
+        "limit": 5,
+        "size": pagination_size
+      },
+      "filtering": {
+        "enabled": true,
+        "delay": 100,
+        "position": "left",
+        "connectors": false,
+        "placeholder": lang.filter_table
+      },
+      "sorting": {
+        "enabled": true
+      },
+      "on": {
+        "ready.ft.table": function(e, ft){
+          table_mailbox_ready(ft, 'tls_policy_table');
+        },
+        "after.ft.paging": function(e, ft){
+          paging_mailbox_after(ft, 'tls_policy_table');
+        }
+      }
+    });
+  }
   function draw_alias_table() {
     ft_alias_table = FooTable.init('#alias_table', {
       "columns": [
@@ -636,7 +704,7 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?alias=' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/alias/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-alias" data-api-url="delete/alias" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
@@ -711,7 +779,7 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?aliasdomain=' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/aliasdomain/' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-alias-domain" data-api-url="delete/alias-domain" data-item="' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.alias_domain) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>' +
               '</div>';
@@ -779,7 +847,7 @@ jQuery(function($){
             }
             item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1;
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?syncjob=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
@@ -850,7 +918,7 @@ jQuery(function($){
             item.script_data = '<pre style="margin:0px">' + escapeHtml(item.script_data) + '</pre>'
             item.filter_type = '<div class="label label-default">' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '</div>'
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?filter=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="/edit/filter/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="filter_item" name="multi_select" value="' + item.id + '" />'
@@ -892,5 +960,6 @@ jQuery(function($){
   draw_filter_table();
   draw_bcc_table();
   draw_recipient_map_table();
+  draw_tls_policy_table();
 
 });

+ 1 - 1
data/web/js/user.js

@@ -122,7 +122,7 @@ jQuery(function($){
             item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1);
             if (acl_data.syncjobs === 1) {
               item.action = '<div class="btn-group">' +
-                '<a href="/edit.php?syncjob=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
                 '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
                 '</div>';
               item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';

+ 34 - 0
data/web/json_api.php

@@ -153,6 +153,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "recipient_map":
             process_add_return(recipient_map('add', $attr));
           break;
+          case "tls-policy-map":
+            process_add_return(tls_policy_maps('add', $attr));
+          break;
         }
       break;
       case "get":
@@ -662,6 +665,31 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               break;
             }
           break;
+          case "tls-policy-map":
+            switch ($object) {
+              case "all":
+                $tls_policy_maps_items = tls_policy_maps('get');
+                if (!empty($tls_policy_maps_items)) {
+                  foreach ($tls_policy_maps_items as $tls_policy_maps_item) {
+                    if ($details = tls_policy_maps('details', $tls_policy_maps_item)) {
+                      $data[] = $details;
+                    }
+                    else {
+                      continue;
+                    }
+                  }
+                }
+                process_get_return($data);
+              break;
+              default:
+                $data = tls_policy_maps('details', $object);
+                if (!empty($data)) {
+                  $data[] = $details;
+                }
+                process_get_return($data);
+              break;
+            }
+          break;
           case "policy_wl_mailbox":
             switch ($object) {
               default:
@@ -919,6 +947,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "recipient_map":
             process_delete_return(recipient_map('delete', array('id' => $items)));
           break;
+          case "tls-policy-map":
+            process_delete_return(tls_policy_maps('delete', array('id' => $items)));
+          break;
           case "fwdhost":
             process_delete_return(fwdhost('delete', array('forwardinghost' => $items)));
           break;
@@ -991,6 +1022,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "recipient_map":
             process_edit_return(recipient_map('edit', array_merge(array('id' => $items), $attr)));
           break;
+          case "tls-policy-map":
+            process_edit_return(tls_policy_maps('edit', array_merge(array('id' => $items), $attr)));
+          break;
           case "alias":
             process_edit_return(mailbox('edit', 'alias', array_merge(array('id' => $items), $attr)));
           break;

+ 21 - 6
data/web/lang/lang.de.php

@@ -240,6 +240,17 @@ $lang['header']['mailcow_settings'] = 'Konfiguration';
 $lang['header']['administration'] = 'Administration';
 $lang['header']['mailboxes'] = 'Mailboxen';
 $lang['header']['user_settings'] = 'Benutzereinstellungen';
+$lang['mailbox']['tls_policy_maps'] = 'TLS-Richtlinien';
+$lang['mailbox']['tls_policy_maps_long'] = 'Ausgehende TLS-Richtlinien';
+$lang['mailbox']['tls_policy_maps_info'] = 'Nachstehende Richtlinien erzwingen TLS-Transportregeln unabhängig von TLS-Richtlinieneinstellungen eines Benutzers.<br>
+  Für weitere Informationen zur Syntax sollte <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">die "smtp_tls_policy_maps" Dokumentation</a> konsultiert werden.';
+$lang['mailbox']['tls_enforce_in'] = 'Enforce TLS incoming';
+$lang['mailbox']['tls_enforce_out'] = 'Enforce TLS outgoing';
+$lang['mailbox']['tls_map_dest'] = 'Ziel';
+$lang['mailbox']['tls_map_dest_info'] = 'Beispiele: example.org, .example.org, mail@example.org, [mail.example.org]:25';
+$lang['mailbox']['tls_map_policy'] = 'Richtlinie';
+$lang['mailbox']['tls_map_parameters'] = 'Parameter';
+$lang['mailbox']['tls_map_parameters_info'] = 'Leer oder Parameter, Beispiele: protocols=!SSLv2 ciphers=medium exclude=3DES';
 $lang['mailbox']['booking_0'] = 'Immer als verfügbar anzeigen';
 $lang['mailbox']['booking_lt0'] = 'Unbegrenzt, jedoch anzeigen, wenn gebucht';
 $lang['mailbox']['booking_custom'] = 'Benutzerdefiniertes Limit';
@@ -666,9 +677,13 @@ $lang['mailbox']['recipient_map_new_info'] = 'Der neue Empfänger muss eine E-Ma
 $lang['mailbox']['recipient_map_old'] = 'Original Empfänger';
 $lang['mailbox']['recipient_map_new'] = 'Neuer Empfänger';
 $lang['mailbox']['add_recipient_map_entry'] = 'Empfängerumschreibung hinzufügen';
-$lang['danger']['invalid_recipient_map_new'] = 'Neuer Empfänger %s ist ungültig';
-$lang['danger']['invalid_recipient_map_old'] = 'Originaler Empfänger %s ist ungültig';
-$lang['danger']['recipient_map_entry_exists'] = 'Eine Empfängerumschreibung für %s existiert bereits';
-$lang['success']['recipient_map_entry_saved'] = 'Empfängerumschreibung für Objekt %s wurde gespeichert';
-$lang['success']['recipient_map_entry_deleted'] = 'Empfängerumschreibung für Objekt %s wurde gelöscht';
-
+$lang['danger']['invalid_recipient_map_new'] = 'Neuer Empfänger "%s" ist ungültig';
+$lang['danger']['invalid_recipient_map_old'] = 'Originaler Empfänger "%s" ist ungültig';
+$lang['danger']['recipient_map_entry_exists'] = 'Eine Empfängerumschreibung für Objekt "%s" existiert bereits';
+$lang['success']['recipient_map_entry_saved'] = 'Empfängerumschreibung für Objekt "%s" wurde gespeichert';
+$lang['success']['recipient_map_entry_deleted'] = 'Empfängerumschreibung mit der ID %s wurde gelöscht';
+$lang['danger']['tls_policy_map_entry_exists'] = 'Eine TLS-Richtlinie "%s" existiert bereits';
+$lang['success']['tls_policy_map_entry_saved'] = 'TLS-Richtlinieneintrag "%s" wurde gespeichert';
+$lang['success']['tls_policy_map_entry_deleted'] = 'TLS-Richtlinie mit der ID %s wurde gelöscht';
+$lang['mailbox']['add_tls_policy_map'] = "TLS-Richtlinieneintrag hinzufügen";
+$lang['danger']['tls_policy_map_parameter_invalid'] = "Parameter ist ungültig";

+ 19 - 3
data/web/lang/lang.en.php

@@ -242,6 +242,17 @@ $lang['header']['mailcow_settings'] = 'Configuration';
 $lang['header']['administration'] = 'Administration';
 $lang['header']['mailboxes'] = 'Mailboxes';
 $lang['header']['user_settings'] = 'User settings';
+$lang['mailbox']['tls_policy_maps'] = 'TLS policy maps';
+$lang['mailbox']['tls_policy_maps_long'] = 'Outgoing TLS policy map overrides';
+$lang['mailbox']['tls_policy_maps_info'] = 'This policy map overrides outgoing TLS transport rules independently of a users TLS policy settings.<br>
+  Please check <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">the "smtp_tls_policy_maps" docs</a> for further information.';
+$lang['mailbox']['tls_enforce_in'] = 'Enforce TLS incoming';
+$lang['mailbox']['tls_enforce_out'] = 'Enforce TLS outgoing';
+$lang['mailbox']['tls_map_dest'] = 'Destination';
+$lang['mailbox']['tls_map_dest_info'] = 'Examples: example.org, .example.org, mail@example.org, [mail.example.org]:25';
+$lang['mailbox']['tls_map_policy'] = 'Policy';
+$lang['mailbox']['tls_map_parameters'] = 'Parameters';
+$lang['mailbox']['tls_map_parameters_info'] = 'Empty or parameters, for example: protocols=!SSLv2 ciphers=medium exclude=3DES';
 $lang['mailbox']['booking_0'] = 'Always show as free';
 $lang['mailbox']['booking_lt0'] = 'Unlimited, but show as busy when booked';
 $lang['mailbox']['booking_custom'] = 'Hard-limit to a custom amount of bookings';
@@ -667,6 +678,7 @@ $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']['add_tls_policy_map'] = "Add TLS policy map";
 $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.<br/>
   The local destination will not be informed about a failed delivery.";
 $lang['mailbox']['address_rewriting'] = 'Address rewriting';
@@ -679,10 +691,14 @@ $lang['mailbox']['recipient_map_old'] = 'Original recipient';
 $lang['mailbox']['recipient_map_new'] = 'New recipient';
 $lang['danger']['invalid_recipient_map_new'] = 'Invalid new recipient specified: %s';
 $lang['danger']['invalid_recipient_map_old'] = 'Invalid original recipient specified: %s';
-$lang['danger']['recipient_map_entry_exists'] = 'A Recipient map entry for %s exists';
-$lang['success']['recipient_map_entry_saved'] = 'Recipient map entry for %s has been saved';
-$lang['success']['recipient_map_entry_deleted'] = 'Recipient map entry for %s has been deleted';
+$lang['danger']['recipient_map_entry_exists'] = 'A Recipient map entry "%s" exists';
+$lang['success']['recipient_map_entry_saved'] = 'Recipient map entry "%s" has been saved';
+$lang['success']['recipient_map_entry_deleted'] = 'Recipient map ID %s has been deleted';
+$lang['danger']['tls_policy_map_entry_exists'] = 'A TLS policy map entry "%s" exists';
+$lang['success']['tls_policy_map_entry_saved'] = 'TLS policy map entry "%s" has been saved';
+$lang['success']['tls_policy_map_entry_deleted'] = 'TLS policy map ID %s has been deleted';
 $lang['mailbox']['add_recipient_map_entry'] = 'Add recipient map';
+$lang['danger']['tls_policy_map_parameter_invalid'] = "Policy parameter is invalid";
 
 $lang['oauth2']['scope_ask_permission'] = 'An application asked for the following permissions';
 $lang['oauth2']['profile'] = 'Profile';

+ 29 - 0
data/web/mailbox.php

@@ -22,6 +22,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
     <li role="presentation"><a href="#tab-syncjobs" aria-controls="tab-syncjobs" role="tab" data-toggle="tab"><?=$lang['mailbox']['sync_jobs'];?></a></li>
     <li role="presentation"><a href="#tab-filters" aria-controls="tab-filters" role="tab" data-toggle="tab"><?=$lang['mailbox']['filters'];?></a></li>
     <li role="presentation"><a href="#tab-bcc" aria-controls="tab-filters" role="tab" data-toggle="tab"><?=$lang['mailbox']['address_rewriting'];?></a></li>
+    <li role="presentation"><a href="#tab-tls-policy" aria-controls="tab-tls-policy" role="tab" data-toggle="tab"><?=$lang['mailbox']['tls_policy_maps'];?></a></li>
   </ul>
 
 	<div class="row">
@@ -287,6 +288,34 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
             </div>
           </div>
         </div>
+
+        <div role="tabpanel" class="tab-pane <?=($_SESSION['mailcow_cc_role'] == "admin") ?: 'hidden';?>" id="tab-tls-policy">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <?=$lang['mailbox']['tls_policy_maps_long'];?> <span class="badge badge-info table-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default refresh_table" data-draw="draw_tls_policy_table" data-table="tls_policy_table"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <p style="margin:10px" class="help-block"><?=$lang['mailbox']['tls_policy_maps_info'];?></p>
+            <div class="table-responsive">
+              <table class="table table-striped" id="tls_policy_table"></table>
+            </div>
+            <div class="mass-actions-mailbox">
+              <div class="btn-group">
+                <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="tls-policy-map" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
+                <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
+                <ul class="dropdown-menu">
+                  <li><a data-action="edit_selected" data-id="tls-policy-map" data-api-url='edit/tls-policy-map' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="tls-policy-map" data-api-url='edit/tls-policy-map' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li role="separator" class="divider"></li>
+                  <li><a data-action="delete_selected" data-id="tls-policy-map" data-api-url='delete/tls-policy-map' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                </ul>
+                <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addTLSPolicyMapAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_tls_policy_map'];?></a>
+              </div>
+            </div>
+          </div>
+        </div>
       </div> <!-- /tab-content -->
     </div> <!-- /col-md-12 -->
   </div> <!-- /row -->

+ 56 - 0
data/web/modals/mailbox.php

@@ -692,6 +692,62 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
   </div>
 </div><!-- add add_recipient_map modal -->
+<!-- add add_tls_policy_map modal -->
+<div class="modal fade" id="addTLSPolicyMapAdmin" tabindex="-1" role="dialog" aria-hidden="true">
+  <div class="modal-dialog modal-lg">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
+        <h3 class="modal-title"><?=$lang['mailbox']['tls_policy_maps'];?></h3>
+      </div>
+      <div class="modal-body">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_tls_policy_map">
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="dest"><?=$lang['mailbox']['tls_map_dest'];?></label>
+            <div class="col-sm-10">
+            <input type="text" class="form-control" name="dest">
+            <small><?=$lang['mailbox']['tls_map_dest_info'];?></small>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="policy"><?=$lang['mailbox']['tls_map_policy'];?>:</label>
+            <div class="col-sm-10">
+              <select class="full-width-select" name="policy" required>
+                <option value="none">none</option>
+                <option value="may">may</option>
+                <option value="encrypt">encrypt</option>
+                <option value="dane">dane</option>
+                <option value="dane-only">dane-only</option>
+                <option value="fingerprint">fingerprint</option>
+                <option value="verify">verify</option>
+                <option value="secure">secure</option>
+              </select>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="parameters"><?=$lang['mailbox']['tls_map_parameters'];?></label>
+            <div class="col-sm-10">
+            <input type="text" class="form-control" name="parameters">
+            <small><?=$lang['mailbox']['tls_map_parameters_info'];?></small>
+            </div>
+          </div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+              <button class="btn btn-success" data-action="add_item" data-id="add_tls_policy_map" data-api-url='add/tls-policy-map' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+						</div>
+					</div>
+				</form>
+      </div>
+    </div>
+  </div>
+</div><!-- add add_tls_policy_map modal -->
 <!-- log modal -->
 <div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
   <div class="modal-dialog modal-lg" role="document">

+ 1 - 1
data/web/user.php

@@ -394,7 +394,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 <div style="margin-bottom:200px;"></div>
 <?php
 }
-if (isset($_SESSION['mailcow_cc_role'])) {
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] != 'admin') {
 require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/user.php';
 ?>
 <script type='text/javascript'>