瀏覽代碼

[Web] Fix require_once to always include document root
[Web] Add system mails (send mails to all mailboxes via LMTP)
[Web] Allow to add more administrators
[Web] Fix domain administrator editing
[Web] Remove some foreign keys
[Web] Remove username from API
[Web] Remove more .php extension from code
[Web] More minor fixes

André 7 年之前
父節點
當前提交
9f0be1d8a8

+ 115 - 34
data/web/admin.php

@@ -1,8 +1,8 @@
 <?php
-require_once("inc/prerequisites.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
-require_once("inc/header.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $tfa_data = get_tfa();
 ?>
@@ -10,6 +10,7 @@ $tfa_data = get_tfa();
   <ul class="nav nav-tabs" role="tablist">
     <li role="presentation" class="active"><a href="#tab-access" aria-controls="tab-access" role="tab" data-toggle="tab"><?=$lang['admin']['access'];?></a></li>
     <li role="presentation"><a href="#tab-config" aria-controls="tab-config" role="tab" data-toggle="tab"><?=$lang['admin']['configuration'];?></a></li>
+    <li role="presentation"><a href="#tab-sys-mails" aria-controls="tab-sys-mails" role="tab" data-toggle="tab">System mails</a></li>
   </ul>
 
   <div class="tab-content" style="padding-top:20px">
@@ -17,34 +18,28 @@ $tfa_data = get_tfa();
     <div class="panel panel-danger">
       <div class="panel-heading"><?=$lang['admin']['admin_details'];?></div>
       <div class="panel-body">
-        <form class="form-horizontal" autocapitalize="none" data-id="admin" autocorrect="off" role="form" method="post">
-        <?php $admindetails = get_admin_details(); ?>
-          <div class="form-group">
-            <label class="control-label col-sm-3" for="admin_user"><?=$lang['admin']['admin'];?>:</label>
-            <div class="col-sm-9">
-              <input type="text" class="form-control" name="admin_user" value="<?=htmlspecialchars($admindetails['username']);?>" required>
-              &rdsh; <kbd>a-z A-Z - _ .</kbd>
-            </div>
-          </div>
-          <div class="form-group">
-            <label class="control-label col-sm-3" for="admin_pass"><?=$lang['admin']['password'];?>:</label>
-            <div class="col-sm-9">
-            <input type="password" data-hibp="true" class="form-control" name="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
-            </div>
-          </div>
-          <div class="form-group">
-            <label class="control-label col-sm-3" for="admin_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
-            <div class="col-sm-9">
-            <input type="password" class="form-control" name="admin_pass2">
-            </div>
-          </div>
-          <div class="form-group">
-            <div class="col-sm-offset-3 col-sm-9">
-              <button class="btn btn-default" data-action="edit_selected" data-id="admin" data-item="admin" data-api-url='edit/self' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
-            </div>
+        <div class="table-responsive">
+          <table class="table table-striped table-condensed" id="adminstable"></table>
+        </div>
+        <div class="mass-actions-admin">
+          <div class="btn-group">
+            <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="admins" 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="admins" data-api-url='edit/admin' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+              <li><a data-action="edit_selected" data-id="admins" data-api-url='edit/admin' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+              <li role="separator" class="divider"></li>
+              <li><a data-action="edit_selected" data-id="admins" data-api-url='edit/admin' data-api-attr='{"disable_tfa":"1"}' href="#"><?=$lang['tfa']['disable_tfa'];?></a></li>
+              <li role="separator" class="divider"></li>
+              <li><a data-action="delete_selected" data-id="admins" data-api-url='delete/admin' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+            </ul>
+            <a class="btn btn-sm btn-success" data-id="add_admin" data-toggle="modal" data-target="#addAdminModal" href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add_admin'];?></a>
           </div>
-        </form>
-        <legend><?=$lang['tfa']['tfa'];?></legend>
+        </div>
+        <legend style="margin-top:20px">
+        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="margin-bottom: -5px;">
+          <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"/>
+        </svg> <?=$lang['tfa']['tfa'];?></legend>
         <div class="row">
           <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?>:</div>
           <div class="col-sm-9 col-xs-7">
@@ -68,7 +63,7 @@ $tfa_data = get_tfa();
         <div class="row">
           <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
           <div class="col-sm-9 col-xs-7">
-            <select data-width="auto" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
+            <select data-width="fit" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
               <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
               <option value="u2f"><?=$lang['tfa']['u2f'];?></option>
               <option value="totp"><?=$lang['tfa']['totp'];?></option>
@@ -79,24 +74,27 @@ $tfa_data = get_tfa();
         <legend data-target="#api" style="margin-top:40px;cursor:pointer" id="api_legend" unselectable="on" data-toggle="collapse">
           <span id="api_arrow" style="font-size:12px" class="rotate glyphicon glyphicon-menu-down"></span> API (experimental, work in progress)
         </legend>
+        <?php
+        $api = admin_api('get');
+        ?>
         <div id="api" class="collapse">
         <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
           <div class="form-group">
             <label class="control-label col-sm-3" for="allow_from"><?=$lang['admin']['api_allow_from'];?>:</label>
             <div class="col-sm-9">
-              <textarea class="form-control" rows="5" name="allow_from" id="allow_from" required><?=htmlspecialchars($admindetails['allow_from']);?></textarea>
+              <textarea class="form-control" rows="5" name="allow_from" id="allow_from" required><?=htmlspecialchars($api['allow_from']);?></textarea>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-3" for="admin_api_key"><?=$lang['admin']['api_key'];?>:</label>
             <div class="col-sm-9">
-              <input type="text" class="form-control" placeholder="-" value="<?=htmlspecialchars($admindetails['api_key']);?>" readonly>
+              <input type="text" class="form-control" placeholder="-" value="<?=htmlspecialchars($api['api_key']);?>" readonly>
             </div>
           </div>
           <div class="form-group">
             <div class="col-sm-offset-3 col-sm-9">
               <label>
-                <input type="checkbox" name="active" <?=($admindetails['api_active'] == 1) ? 'checked' : null;?>> <?=$lang['admin']['activate_api'];?>
+                <input type="checkbox" name="active" <?=($api['active'] == 1) ? 'checked' : null;?>> <?=$lang['admin']['activate_api'];?>
               </label>
             </div>
           </div>
@@ -117,7 +115,7 @@ $tfa_data = get_tfa();
     <div class="panel-heading"><?=$lang['admin']['domain_admins'];?></div>
         <div class="panel-body">
           <div class="table-responsive">
-            <table class="table table-striped" id="domainadminstable"></table>
+            <table class="table table-striped table-condensed" id="domainadminstable"></table>
           </div>
           <div class="mass-actions-admin">
             <div class="btn-group">
@@ -776,6 +774,88 @@ $tfa_data = get_tfa();
   </div>
   </div>
 
+  <div role="tabpanel" class="tab-pane" id="tab-sys-mails">
+    <div class="panel panel-default">
+      <div class="panel-heading"><?=$lang['admin']['sys_mails'];?></div>
+      <div class="panel-body">
+        <form class="form-horizontal" autocapitalize="none" data-id="admin" autocorrect="off" role="form" method="post">
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="mass_from"><?=$lang['admin']['from'];?>:</label>
+            <div class="col-sm-10">
+              <input type="email" class="form-control" name="mass_from" value="noreply@<?=getenv('MAILCOW_HOSTNAME');;?>" required>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="mass_subject"><?=$lang['admin']['subject'];?>:</label>
+            <div class="col-sm-10">
+              <input type="text" class="form-control" name="mass_subject" required>
+            </div>
+          </div>
+          <?php
+          $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
+          if (!empty($domains)) {
+            foreach ($domains as $domain) {
+              foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
+                $mailboxes[] = $mailbox;
+              }
+            }
+          }
+          ?>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="mass_subject"><?=$lang['admin']['include_exclude'];?>:
+              <p class="help-block"><?=$lang['admin']['include_exclude_info'];?></p>
+            </label>
+            <div class="col-sm-5">
+              <label class="control-label" for="mass_exclude"><?=$lang['admin']['excludes'];?>:</label>
+              <select id="mass_exclude" name="mass_exclude[]" data-live-search="true" data-width="100%"  size="30" multiple>
+              <?php
+              if (!empty($mailboxes)) {
+                foreach (array_filter($mailboxes) as $mailbox):
+                ?>
+                <option><?=htmlspecialchars($mailbox);?></option>
+                <?php
+                endforeach;
+              }
+              ?>
+              </select>
+            </div>
+            <div class="col-sm-5">
+              <label class="control-label" for="mass_include"><?=$lang['admin']['includes'];?>:</label>
+              <select id="mass_include" name="mass_include[]" data-live-search="true" data-width="100%"  size="30" multiple>
+              <?php
+              if (!empty($mailboxes)) {
+                foreach (array_filter($mailboxes) as $mailbox):
+                ?>
+                <option><?=htmlspecialchars($mailbox);?></option>
+                <?php
+                endforeach;
+              }
+              ?>
+              </select>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="mass_text"><?=$lang['admin']['text'];?>:</label>
+            <div class="col-sm-10">
+              <textarea class="form-control" rows="10" name="mass_text" id="mass_text" required></textarea>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+              <label>
+                <input type="checkbox" id="mass_disarm"> <?=$lang['admin']['activate_send'];?>
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+              <button class="btn btn-default" type="submit" id="mass_send" name="mass_send" disabled><span class="glyphicon glyphicon-envelope"></span> Send</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+
   </div>
 </div> <!-- /container -->
 <?php
@@ -785,6 +865,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/admin.php';
 <?php
 $lang_admin = json_encode($lang['admin']);
 echo "var lang = ". $lang_admin . ";\n";
+echo "var admin_username = '". $_SESSION['mailcow_cc_username'] . "';\n";
 echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
 echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n";

+ 1 - 1
data/web/autoconfig.php

@@ -1,5 +1,5 @@
 <?php
-require_once 'inc/vars.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
 $default_autodiscover_config = $autodiscover_config;
 if(file_exists('inc/vars.local.inc.php')) {
   include_once 'inc/vars.local.inc.php';

+ 2 - 2
data/web/autodiscover-json.php

@@ -1,6 +1,6 @@
 <?php
-require_once 'inc/vars.inc.php';
-require_once 'inc/functions.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
 $default_autodiscover_config = $autodiscover_config;
 if(file_exists('inc/vars.local.inc.php')) {
   include_once 'inc/vars.local.inc.php';

+ 2 - 2
data/web/autodiscover.php

@@ -1,6 +1,6 @@
 <?php
-require_once 'inc/vars.inc.php';
-require_once 'inc/functions.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
 $default_autodiscover_config = $autodiscover_config;
 if(file_exists('inc/vars.local.inc.php')) {
   include_once 'inc/vars.local.inc.php';

+ 3 - 0
data/web/css/admin.css

@@ -68,3 +68,6 @@ body.modal-open {
 .table-condensed .input-sm {
   width: 100%!important;  
 }
+.table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
+  padding: 3px;
+}

+ 2 - 2
data/web/debug.php

@@ -1,8 +1,8 @@
 <?php
-require_once "inc/prerequisites.inc.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
-require_once "inc/header.inc.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 
 ?>

+ 177 - 126
data/web/edit.php

@@ -1,11 +1,11 @@
 <?php
-require_once("inc/prerequisites.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 $AuthUsers = array("admin", "domainadmin", "user");
 if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
 	header('Location: /');
 	exit();
 }
-require_once("inc/header.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 ?>
 <div class="container">
 	<div class="row">
@@ -18,149 +18,47 @@ require_once("inc/header.inc.php");
 <?php
 if (isset($_SESSION['mailcow_cc_role'])) {
   if ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin") {
-      if (isset($_GET["alias"]) &&
-        !empty($_GET["alias"])) {
-          $alias = html_entity_decode(rawurldecode($_GET["alias"]));
-          $result = mailbox('get', 'alias_details', $alias);
-          if (!empty($result)) {
-          ?>
-            <h4><?=$lang['edit']['alias'];?></h4>
-            <br />
-            <form class="form-horizontal" data-id="editalias" role="form" method="post">
-              <input type="hidden" value="0" name="active">
-              <div class="form-group">
-                <label class="control-label col-sm-2" for="address"><?=$lang['edit']['alias'];?></label>
-                <div class="col-sm-10">
-                  <input class="form-control" type="text" name="address" value="<?=htmlspecialchars($result['address']);?>" />
-                </div>
-              </div>
-              <div class="form-group">
-                <label class="control-label col-sm-2" for="goto"><?=$lang['edit']['target_address'];?></label>
-                <div class="col-sm-10">
-                  <textarea id="textarea_alias_goto" class="form-control" autocapitalize="none" autocorrect="off" rows="10" id="goto" name="goto" required><?= (!preg_match('/^(null|ham|spam)@localhost$/i', $result['goto'])) ? htmlspecialchars($result['goto']) : null; ?></textarea>
-                  <div class="checkbox">
-                    <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_null" <?= ($result['goto'] == "null@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_null'];?></label>
-                  </div>
-                  <div class="checkbox">
-                    <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_spam" <?= ($result['goto'] == "spam@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_spam'];?></label>
-                  </div>
-                  <div class="checkbox">
-                    <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_ham" <?= ($result['goto'] == "ham@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_ham'];?></label>
-                  </div>
-                </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="editalias" data-item="<?=htmlspecialchars($alias);?>" data-api-url='edit/alias' 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
-          }
-      }
-      elseif (isset($_GET['domainadmin']) &&
-          ctype_alnum(str_replace(array('_', '.', '-'), '', $_GET["domainadmin"])) &&
-          !empty($_GET["domainadmin"]) &&
-          $_GET["domainadmin"] != 'admin' &&
-          $_SESSION['mailcow_cc_role'] == "admin") {
-          $domain_admin = $_GET["domainadmin"];
-          $result = domain_admin('details', $domain_admin);
-          if (!empty($result)) {
-          ?>
-          <h4><?=$lang['edit']['domain_admin'];?></h4>
+    if (isset($_GET["alias"]) &&
+      !empty($_GET["alias"])) {
+        $alias = html_entity_decode(rawurldecode($_GET["alias"]));
+        $result = mailbox('get', 'alias_details', $alias);
+        if (!empty($result)) {
+        ?>
+          <h4><?=$lang['edit']['alias'];?></h4>
           <br />
-          <form class="form-horizontal" data-id="editdomainadmin" role="form" method="post">
+          <form class="form-horizontal" data-id="editalias" role="form" method="post">
             <input type="hidden" value="0" name="active">
             <div class="form-group">
-              <label class="control-label col-sm-2" for="username_new"><?=$lang['edit']['username'];?></label>
-              <div class="col-sm-10">
-                <input class="form-control" type="text" name="username_new" value="<?=htmlspecialchars($domain_admin);?>" />
-              </div>
-            </div>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="domains"><?=$lang['edit']['domains'];?></label>
-              <div class="col-sm-10">
-                <select data-live-search="true" class="full-width-select" name="domains" multiple required>
-                <?php
-                foreach ($result['selected_domains'] as $domain):
-                ?>
-                  <option selected><?=htmlspecialchars($domain);?></option>
-                <?php
-                endforeach;
-                foreach ($result['unselected_domains'] as $domain):
-                ?>
-                  <option><?=htmlspecialchars($domain);?></option>
-                <?php
-                endforeach;
-                ?>
-                </select>
-              </div>
-            </div>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
+              <label class="control-label col-sm-2" for="address"><?=$lang['edit']['alias'];?></label>
               <div class="col-sm-10">
-              <input type="password" data-hibp="true" class="form-control" name="password" placeholder="">
+                <input class="form-control" type="text" name="address" value="<?=htmlspecialchars($result['address']);?>" />
               </div>
             </div>
             <div class="form-group">
-              <label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
+              <label class="control-label col-sm-2" for="goto"><?=$lang['edit']['target_address'];?></label>
               <div class="col-sm-10">
-              <input type="password" class="form-control" name="password2">
-              </div>
-            </div>
-            <div class="form-group">
-              <div class="col-sm-offset-2 col-sm-10">
+                <textarea id="textarea_alias_goto" class="form-control" autocapitalize="none" autocorrect="off" rows="10" id="goto" name="goto" required><?= (!preg_match('/^(null|ham|spam)@localhost$/i', $result['goto'])) ? htmlspecialchars($result['goto']) : null; ?></textarea>
                 <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>
+                  <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_null" <?= ($result['goto'] == "null@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_null'];?></label>
+                </div>
+                <div class="checkbox">
+                  <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_spam" <?= ($result['goto'] == "spam@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_spam'];?></label>
+                </div>
+                <div class="checkbox">
+                  <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_ham" <?= ($result['goto'] == "ham@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_ham'];?></label>
                 </div>
               </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="disable_tfa"> <?=$lang['tfa']['disable_tfa'];?></label>
+                <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="editdomainadmin" data-item="<?=$domain_admin;?>" data-api-url='edit/domain-admin' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
-              </div>
-            </div>
-          </form>
-          <form data-id="daacl" class="form-inline well" method="post">
-            <div class="row">
-              <div class="col-sm-1">
-                <p class="help-block">ACL</p>
-              </div>
-              <div class="col-sm-10">
-                <div class="form-group">
-                  <select id="da_acl" name="da_acl" size="10" multiple>
-                  <?php
-                  $da_acls = acl('get', 'domainadmin', $domain_admin);
-                  foreach ($da_acls as $acl => $val):
-                    ?>
-                    <option value="<?=$acl;?>" <?=($val == 1) ? 'selected' : null;?>><?=$lang['acl'][$acl];?></option>
-                    <?php
-                  endforeach;
-                  ?>
-                  </select>
-                </div>
-                <div class="form-group">
-                  <button class="btn btn-default" data-action="edit_selected" data-id="daacl" data-item="<?=htmlspecialchars($domain_admin);?>" data-api-url='edit/da-acl' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
-                </div>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editalias" data-item="<?=htmlspecialchars($alias);?>" data-api-url='edit/alias' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -172,6 +70,159 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         <?php
         }
     }
+    elseif (isset($_GET['domainadmin'])) {
+      $domain_admin = $_GET["domainadmin"];
+      $result = domain_admin('details', $domain_admin);
+      if (!empty($result)) {
+      ?>
+      <h4><?=$lang['edit']['domain_admin'];?></h4>
+      <br />
+      <form class="form-horizontal" data-id="editdomainadmin" role="form" method="post">
+        <input type="hidden" value="0" name="active">
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="username_new"><?=$lang['edit']['username'];?></label>
+          <div class="col-sm-10">
+            <input class="form-control" type="text" name="username_new" value="<?=htmlspecialchars($domain_admin);?>" />
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="domains"><?=$lang['edit']['domains'];?></label>
+          <div class="col-sm-10">
+            <select data-live-search="true" class="full-width-select" name="domains" multiple required>
+            <?php
+            foreach ($result['selected_domains'] as $domain):
+            ?>
+              <option selected><?=htmlspecialchars($domain);?></option>
+            <?php
+            endforeach;
+            foreach ($result['unselected_domains'] as $domain):
+            ?>
+              <option><?=htmlspecialchars($domain);?></option>
+            <?php
+            endforeach;
+            ?>
+            </select>
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
+          <div class="col-sm-10">
+          <input type="password" data-hibp="true" class="form-control" name="password" placeholder="">
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
+          <div class="col-sm-10">
+          <input type="password" class="form-control" name="password2">
+          </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">
+            <div class="checkbox">
+            <label><input type="checkbox" value="1" name="disable_tfa"> <?=$lang['tfa']['disable_tfa'];?></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-api-reload-location="/admin" data-id="editdomainadmin" data-item="<?=$domain_admin;?>" data-api-url='edit/domain-admin' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+          </div>
+        </div>
+      </form>
+      <form data-id="daacl" class="form-inline well" method="post">
+        <div class="row">
+          <div class="col-sm-1">
+            <p class="help-block">ACL</p>
+          </div>
+          <div class="col-sm-10">
+            <div class="form-group">
+              <select id="da_acl" name="da_acl" size="10" multiple>
+              <?php
+              $da_acls = acl('get', 'domainadmin', $domain_admin);
+              foreach ($da_acls as $acl => $val):
+                ?>
+                <option value="<?=$acl;?>" <?=($val == 1) ? 'selected' : null;?>><?=$lang['acl'][$acl];?></option>
+                <?php
+              endforeach;
+              ?>
+              </select>
+            </div>
+            <div class="form-group">
+              <button class="btn btn-default" data-action="edit_selected" data-id="daacl" data-item="<?=htmlspecialchars($domain_admin);?>" data-api-url='edit/da-acl' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+            </div>
+          </div>
+        </div>
+      </form>
+      <?php
+      }
+      else {
+      ?>
+        <div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+      <?php
+      }
+    }
+    elseif (isset($_GET['admin'])) {
+      $admin = $_GET["admin"];
+      $result = admin('details', $admin);
+      if (!empty($result)) {
+      ?>
+      <h4><?=$lang['edit']['domain_admin'];?></h4>
+      <br />
+      <form class="form-horizontal" data-id="editadmin" role="form" method="post">
+        <input type="hidden" value="0" name="active">
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="username_new"><?=$lang['edit']['username'];?></label>
+          <div class="col-sm-10">
+            <input class="form-control" type="text" name="username_new" value="<?=htmlspecialchars($admin);?>" />
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
+          <div class="col-sm-10">
+          <input type="password" data-hibp="true" class="form-control" name="password" placeholder="">
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
+          <div class="col-sm-10">
+          <input type="password" class="form-control" name="password2">
+          </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">
+            <div class="checkbox">
+            <label><input type="checkbox" value="1" name="disable_tfa"> <?=$lang['tfa']['disable_tfa'];?></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-api-reload-location="/admin" data-id="editadmin" data-item="<?=$admin;?>" data-api-url='edit/admin' 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
+      }
+    }
     elseif (isset($_GET['domain']) &&
       is_valid_domain_name($_GET["domain"]) &&
       !empty($_GET["domain"])) {
@@ -1085,5 +1136,5 @@ echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 <script src="/js/footable.min.js"></script>
 <script src="/js/edit.js"></script>
 <?php
-require_once("inc/footer.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
 ?>

+ 276 - 0
data/web/inc/functions.admin.inc.php

@@ -0,0 +1,276 @@
+<?php
+function admin($_action, $_data = null) {
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'][] = array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_action, $_data_log),
+      'msg' => 'access_denied'
+    );
+    return false;
+  }
+  global $pdo;
+  global $lang;
+  $_data_log = $_data;
+  !isset($_data_log['password']) ?: $_data_log['password'] = '*';
+  !isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
+  switch ($_action) {
+    case 'add':
+      $username		= strtolower(trim($_data['username']));
+      $password		= $_data['password'];
+      $password2  = $_data['password2'];
+      $active     = intval($_data['active']);
+      if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => 'username_invalid'
+        );
+        return false;
+      }
+
+      $stmt = $pdo->prepare("SELECT `username` FROM `admin`
+        WHERE `username` = :username");
+      $stmt->execute(array(':username' => $username));
+      $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+
+      $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
+        WHERE `username` = :username");
+      $stmt->execute(array(':username' => $username));
+      $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+
+      foreach ($num_results as $num_results_each) {
+        if ($num_results_each != 0) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => array('object_exists', htmlspecialchars($username))
+          );
+          return false;
+        }
+      }
+      if (!empty($password) && !empty($password2)) {
+        if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => 'password_complexity'
+          );
+          return false;
+        }
+        if ($password != $password2) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => 'password_mismatch'
+          );
+          return false;
+        }
+        $password_hashed = hash_password($password);
+        $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
+          VALUES (:username, :password_hashed, '1', :active)");
+        $stmt->execute(array(
+          ':username' => $username,
+          ':password_hashed' => $password_hashed,
+          ':active' => $active
+        ));
+      }
+      else {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => 'password_empty'
+        );
+        return false;
+      }
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data_log),
+        'msg' => array('admin_added', htmlspecialchars($username))
+      );
+    break;
+    case 'edit':
+      if (!is_array($_data['username'])) {
+        $usernames = array();
+        $usernames[] = $_data['username'];
+      }
+      else {
+        $usernames = $_data['username'];
+      }
+      foreach ($usernames as $username) {
+        $is_now = admin('details', $username);
+        if (!empty($is_now)) {
+          $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+          $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
+        }
+        else {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => 'access_denied'
+          );
+          continue;
+        }
+        $password     = $_data['password'];
+        $password2    = $_data['password2'];
+        if ($active == 0) {
+          $left_active = 0;
+          foreach (admin('get') as $admin) {
+            $left_active = $left_active + admin('details', $admin)['active_int'];
+          }
+          if ($left_active == 1) {
+            $_SESSION['return'][] = array(
+              'type' => 'warning',
+              'log' => array(__FUNCTION__, $_action, $_data_log),
+              'msg' => 'no_active_admin'
+            );
+            continue;
+          }
+        }
+        if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => array('username_invalid', $username_new)
+          );
+          continue;
+        }
+        if ($username_new != $username) {
+          if (!empty(admin('details', $username_new)['username'])) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data_log),
+              'msg' => array('username_invalid', $username_new)
+            );
+            continue;
+          }
+        }
+        if (!empty($password) && !empty($password2)) {
+          if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data_log),
+              'msg' => 'password_complexity'
+            );
+            continue;
+          }
+          if ($password != $password2) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data_log),
+              'msg' => 'password_mismatch'
+            );
+            continue;
+          }
+          $password_hashed = hash_password($password);
+          $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
+          $stmt->execute(array(
+            ':password_hashed' => $password_hashed,
+            ':username_new' => $username_new,
+            ':username' => $username,
+            ':active' => $active
+          ));
+          if (isset($_data['disable_tfa'])) {
+            $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
+            $stmt->execute(array(':username' => $username));
+          }
+          else {
+            $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
+            $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
+          }
+        }
+        else {
+          $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
+          $stmt->execute(array(
+            ':username_new' => $username_new,
+            ':username' => $username,
+            ':active' => $active
+          ));
+          if (isset($_data['disable_tfa'])) {
+            $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
+            $stmt->execute(array(':username' => $username));
+          }
+          else {
+            $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
+            $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
+          }
+        }
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => array('admin_modified', htmlspecialchars($username))
+        );
+      }
+      return true;
+    break;
+    case 'delete':
+      $usernames = (array)$_data['username'];
+      foreach ($usernames as $username) {
+        if ($_SESSION['mailcow_cc_role'] == $username) {
+          $_SESSION['return'][] = array(
+            'type' => 'warning',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => 'cannot_delete_self'
+          );
+          continue;
+        }
+        if (empty(admin('details', $username))) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => array('username_invalid', $username)
+          );
+          continue;
+        }
+        $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
+        $stmt->execute(array(
+          ':username' => $username,
+        ));
+        $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+        $stmt->execute(array(
+          ':username' => $username,
+        ));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => array('admin_removed', htmlspecialchars($username))
+        );
+      }
+    break;
+    case 'get':
+      $admins = array();
+      $stmt = $pdo->query("SELECT `username` FROM `admin` WHERE `superadmin` = '1'");
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while ($row = array_shift($rows)) {
+        $admins[] = $row['username'];
+      }
+      return $admins;
+    break;
+    case 'details':
+      $admindata = array();
+      $stmt = $pdo->prepare("SELECT
+        `tfa`.`active` AS `tfa_active_int`,
+        CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`,
+        `admin`.`username`,
+        `admin`.`created`,
+        `admin`.`active` AS `active_int`,
+        CASE `admin`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+          FROM `admin`
+          LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
+            WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
+      $stmt->execute(array(
+        ':admin' => $_data
+      ));
+      $row = $stmt->fetch(PDO::FETCH_ASSOC);
+      if (empty($row)) { 
+        return false;
+      }
+      $admindata['username'] = $row['username'];
+      $admindata['tfa_active'] = $row['tfa_active'];
+      $admindata['active'] = $row['active'];
+      $admindata['tfa_active_int'] = $row['tfa_active_int'];
+      $admindata['active_int'] = $row['active_int'];
+      $admindata['created'] = $row['created'];
+      return $admindata;
+    break;
+  }
+}

+ 11 - 10
data/web/inc/functions.domain_admin.inc.php

@@ -1,5 +1,4 @@
 <?php
-
 function domain_admin($_action, $_data = null) {
   global $pdo;
   global $lang;
@@ -195,6 +194,11 @@ function domain_admin($_action, $_data = null) {
           $stmt->execute(array(
             ':username' => $username,
           ));
+          $stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
+          $stmt->execute(array(
+            ':username_new' => $username_new,
+            ':username' => $username
+          ));
           if (!empty($domains)) {
             foreach ($domains as $domain) {
               $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
@@ -277,7 +281,6 @@ function domain_admin($_action, $_data = null) {
             WHERE `username` = :user");
         $stmt->execute(array(':user' => $username));
         $row = $stmt->fetch(PDO::FETCH_ASSOC);
-
         if (!verify_hash($row['password'], $password_old)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
@@ -286,7 +289,6 @@ function domain_admin($_action, $_data = null) {
           );
           return false;
         }
-
         if (!empty($password_new2) && !empty($password_new)) {
           if ($password_new2 != $password_new) {
             $_SESSION['return'][] = array(
@@ -329,7 +331,7 @@ function domain_admin($_action, $_data = null) {
       }
       $usernames = (array)$_data['username'];
       foreach ($usernames as $username) {
-        if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+        if (empty(domain_admin('details', $username))) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -345,6 +347,10 @@ function domain_admin($_action, $_data = null) {
         $stmt->execute(array(
           ':username' => $username,
         ));
+        $stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
+        $stmt->execute(array(
+          ':username' => $username,
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -362,7 +368,6 @@ function domain_admin($_action, $_data = null) {
         );
         return false;
       }
-
       $stmt = $pdo->query("SELECT DISTINCT
         `username`
           FROM `domain_admins` 
@@ -374,23 +379,19 @@ function domain_admin($_action, $_data = null) {
       while ($row = array_shift($rows)) {
         $domainadmins[] = $row['username'];
       }
-
       return $domainadmins;
     break;
     case 'details':
       $domainadmindata = array();
-
       if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
         return false;
       }
       elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
         return false;
       }
-
       if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
         return false;
       }
-
       $stmt = $pdo->prepare("SELECT
         `tfa`.`active` AS `tfa_active_int`,
         CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`,
@@ -413,7 +414,7 @@ function domain_admin($_action, $_data = null) {
       $domainadmindata['active'] = $row['active'];
       $domainadmindata['tfa_active_int'] = $row['tfa_active_int'];
       $domainadmindata['active_int'] = $row['active_int'];
-      $domainadmindata['modified'] = $row['created'];
+      $domainadmindata['created'] = $row['created'];
       // GET SELECTED
       $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
         WHERE `domain` IN (

+ 153 - 146
data/web/inc/functions.inc.php

@@ -28,7 +28,99 @@ function flush_memcached() {
     // Dunno
   }
 }
-
+function sys_mail($_data) {
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'][] =  array(
+			'type' => 'danger',
+      'log' => array(__FUNCTION__),
+			'msg' => 'access_denied'
+		);
+		return false;
+	}
+  $excludes = $_data['mass_exclude'];
+  $includes = $_data['mass_include'];
+  $mailboxes = array();
+  $mass_from = $_data['mass_from'];
+  $mass_text = $_data['mass_text'];
+  $mass_subject = $_data['mass_subject'];
+  if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'][] =  array(
+			'type' => 'danger',
+      'log' => array(__FUNCTION__),
+			'msg' => 'From address must be a valid email address'
+		);
+		return false;
+  }
+  if (empty($mass_subject)) {
+		$_SESSION['return'][] =  array(
+			'type' => 'danger',
+      'log' => array(__FUNCTION__),
+			'msg' => 'Subject must not be empty'
+		);
+		return false;
+  }
+  if (empty($mass_text)) {
+		$_SESSION['return'][] =  array(
+			'type' => 'danger',
+      'log' => array(__FUNCTION__),
+			'msg' => 'Text must not be empty'
+		);
+		return false;
+  }
+  $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
+  foreach ($domains as $domain) {
+    foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
+      $mailboxes[] = $mailbox;
+    }
+  }
+  if (!empty($includes)) {
+    $rcpts = array_intersect($mailboxes, $includes);
+  }
+  elseif (!empty($excludes)) {
+    $rcpts = array_diff($mailboxes, $excludes);
+  }
+  else {
+    $rcpts = $mailboxes;
+  }
+  if (!empty($rcpts)) {
+    ini_set('max_execution_time', 0);
+    ini_set('max_input_time', 0);
+    $mail = new PHPMailer;
+    $mail->Timeout = 10;
+    $mail->SMTPOptions = array(
+      'ssl' => array(
+        'verify_peer' => false,
+        'verify_peer_name' => false,
+        'allow_self_signed' => true
+      )
+    );
+    $mail->isSMTP();
+    $mail->Host = 'dovecot-mailcow';
+    $mail->SMTPAuth = false;
+    $mail->Port = 24;
+    $mail->setFrom($mass_from);
+    $mail->Subject = $mass_subject;
+    $mail->CharSet ="UTF-8";
+    $mail->Body = $mass_text;
+    $mail->XMailer = 'MooMassMail';
+    foreach ($rcpts as $rcpt) {
+      $mail->AddAddress($rcpt);
+      if (!$mail->send()) {
+        $_SESSION['return'][] =  array(
+          'type' => 'warning',
+          'log' => array(__FUNCTION__),
+          'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
+        );
+      }
+      $mail->ClearAllRecipients();
+    }
+  }
+  $_SESSION['return'][] =  array(
+    'type' => 'success',
+    'log' => array(__FUNCTION__),
+    'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
+  );
+}
 function logger($_data = false) {
   /*
   logger() will be called as last function
@@ -106,21 +198,35 @@ function hasDomainAccess($username, $role, $domain) {
 	if (empty($domain) || !is_valid_domain_name($domain)) {
 		return false;
 	}
-	if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
+	if ($role != 'admin' && $role != 'domainadmin') {
 		return false;
 	}
-  $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
-  WHERE (
-    `active`='1'
-    AND `username` = :username
-    AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
-  )
-  OR 'admin' = :role");
-  $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain, ':role' => $role));
-  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-	if (!empty($num_results)) {
-		return true;
-	}
+  if ($role == 'admin') {
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` = :domain");
+    $stmt->execute(array(':domain' => $domain));
+    $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
+      WHERE `alias_domain` = :domain");
+    $stmt->execute(array(':domain' => $domain));
+    $num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      return true;
+    }
+  }
+  elseif ($role == 'domainadmin') {
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
+    WHERE (
+      `active`='1'
+      AND `username` = :username
+      AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
+    )");
+    $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
+    $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if (!empty($num_results)) {
+      return true;
+    }
+  }
 	return false;
 }
 function hasMailboxObjectAccess($username, $role, $object) {
@@ -318,6 +424,9 @@ function check_login($user, $pass) {
       }
       else {
         unset($_SESSION['ldelay']);
+        // Reactivate TFA if it was set to "deactivate TFA for next login"
+        $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+        $stmt->execute(array(':user' => $user));
         $_SESSION['return'][] =  array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $user, '*'),
@@ -407,110 +516,6 @@ function formatBytes($size, $precision = 2) {
 	}
 	return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
 }
-function edit_admin_account($_data) {
-	global $lang;
-	global $pdo;
-  $_data_log = $_data;
-  !isset($_data_log['admin_pass']) ?: $_data_log['admin_pass'] = '*';
-  !isset($_data_log['admin_pass2']) ?: $_data_log['admin_pass2'] = '*';
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'][] =  array(
-      'type' => 'danger',
-      'log' => array(__FUNCTION__, $_data_log),
-			'msg' => 'access_denied'
-		);
-		return false;
-	}
-	$username_now   = $_SESSION['mailcow_cc_username'];
-	$username       = $_data['admin_user'];
-  $password       = $_data['admin_pass'];
-  $password2      = $_data['admin_pass2'];
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
-		$_SESSION['return'][] =  array(
-			'type' => 'danger',
-      'log' => array(__FUNCTION__, $_data_log),
-			'msg' => 'username_invalid'
-		);
-		return false;
-	}
-	if (!empty($password) && !empty($password2)) {
-    if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-        'msg' => 'password_complexity'
-      );
-      return false;
-    }
-		if ($password != $password2) {
-			$_SESSION['return'][] =  array(
-				'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-				'msg' => 'password_mismatch'
-			);
-			return false;
-		}
-		$password_hashed = hash_password($password);
-		try {
-			$stmt = $pdo->prepare("UPDATE `admin` SET 
-				`password` = :password_hashed,
-				`username` = :username1
-					WHERE `username` = :username2");
-			$stmt->execute(array(
-				':password_hashed' => $password_hashed,
-				':username1' => $username,
-				':username2' => $username_now
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'][] =  array(
-				'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-				'msg' => array('mysql_error', $e)
-			);
-			return false;
-		}
-	}
-	else {
-		try {
-			$stmt = $pdo->prepare("UPDATE `admin` SET 
-				`username` = :username1
-					WHERE `username` = :username2");
-			$stmt->execute(array(
-				':username1' => $username,
-				':username2' => $username_now
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'][] =  array(
-				'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-				'msg' => array('mysql_error', $e)
-			);
-			return false;
-		}
-	}
-	try {
-		$stmt = $pdo->prepare("UPDATE `domain_admins` SET `domain` = 'ALL', `username` = :username1 WHERE `username` = :username2");
-		$stmt->execute(array(':username1' => $username, ':username2' => $username_now));
-		$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username1 WHERE `username` = :username2");
-		$stmt->execute(array(':username1' => $username, ':username2' => $username_now));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'][] =  array(
-			'type' => 'danger',
-      'log' => array(__FUNCTION__, $_data_log),
-			'msg' => array('mysql_error', $e)
-		);
-		return false;
-	}
-  $_SESSION['mailcow_cc_username'] = $username;
-	$_SESSION['return'][] =  array(
-		'type' => 'success',
-    'log' => array(__FUNCTION__, $_data_log),
-		'msg' => 'admin_modified'
-	);
-}
 function update_sogo_static_view() {
   global $pdo;
   global $lang;
@@ -1113,6 +1118,11 @@ function admin_api($action, $data = null) {
       $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
       foreach ($allow_from as $key => $val) {
         if (!filter_var($val, FILTER_VALIDATE_IP)) {
+          $_SESSION['return'][] =  array(
+            'type' => 'warning',
+            'log' => array(__FUNCTION__, $data),
+            'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
+          );
           unset($allow_from[$key]);
           continue;
         }
@@ -1133,16 +1143,24 @@ function admin_api($action, $data = null) {
         strtoupper(bin2hex(random_bytes(3))),
         strtoupper(bin2hex(random_bytes(3)))
       ));
-      $stmt = $pdo->prepare("INSERT INTO `api` (`username`, `api_key`, `active`, `allow_from`)
-        SELECT `username`, :api_key, :active, :allow_from FROM `admin` WHERE `superadmin`='1' AND `active`='1'
-        ON DUPLICATE KEY UPDATE `active` = :active_u, `allow_from` = :allow_from_u ;");
-      $stmt->execute(array(
-        ':api_key' => $api_key,
-        ':active' => $active,
-        ':active_u' => $active,
-        ':allow_from' => $allow_from,
-        ':allow_from_u' => $allow_from
-      ));
+      $stmt = $pdo->query("SELECT `api_key` FROM `api`");
+      $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if (empty($num_results)) {
+        $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `active`, `allow_from`)
+          VALUES (:api_key, :active, :allow_from);");
+        $stmt->execute(array(
+          ':api_key' => $api_key,
+          ':active' => $active,
+          ':allow_from' => $allow_from
+        ));
+      }
+      else {
+        $stmt = $pdo->prepare("UPDATE `api` SET `active` = :active, `allow_from` = :allow_from ;");
+        $stmt->execute(array(
+          ':active' => $active,
+          ':allow_from' => $allow_from
+        ));
+      }
     break;
     case "regen_key":
       $api_key = implode('-', array(
@@ -1152,17 +1170,21 @@ function admin_api($action, $data = null) {
         strtoupper(bin2hex(random_bytes(3))),
         strtoupper(bin2hex(random_bytes(3)))
       ));
-      $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `username` IN
-        (SELECT `username` FROM `admin` WHERE `superadmin`='1' AND `active`='1')");
+      $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key");
       $stmt->execute(array(
         ':api_key' => $api_key
       ));
     break;
+    case "get":
+      $stmt = $pdo->query("SELECT * FROM `api`");
+      $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
+      return $apidata;
+    break;
   }
 	$_SESSION['return'][] =  array(
 		'type' => 'success',
     'log' => array(__FUNCTION__, $data),
-		'msg' => 'admin_modified'
+		'msg' => 'admin_api_modified'
 	);
 }
 function rspamd_ui($action, $data = null) {
@@ -1233,21 +1255,6 @@ function rspamd_ui($action, $data = null) {
     break;
   }
 }
-function get_admin_details() {
-  // No parameter to be given, only one admin should exist
-	global $pdo;
-	global $lang;
-  $data = array();
-  if ($_SESSION['mailcow_cc_role'] != 'admin') {
-    return false;
-  }
-  $stmt = $pdo->query("SELECT `admin`.`username`, `api`.`active` AS `api_active`, `api`.`api_key`, `api`.`allow_from` FROM `admin`
-    LEFT OUTER JOIN `api` ON `admin`.`username` = `api`.`username`
-    WHERE `admin`.`superadmin`='1'
-      AND `admin`.`active`='1'");
-  $data = $stmt->fetch(PDO::FETCH_ASSOC);
-  return $data;
-}
 function get_u2f_registrations($username) {
   global $pdo;
   $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");

+ 3 - 4
data/web/inc/functions.mailbox.inc.php

@@ -2277,7 +2277,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
-            $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` != 'ALL' AND `domain` = :domain");
+            $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain");
             $stmt->execute(array(
               ':domain' => $_data,
             ));
@@ -2535,7 +2535,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
-            $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `domain` != 'ALL' AND `domain` = :domain");
+            $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `domain` = :domain");
             $stmt->execute(array(
               ':domain' => $_data,
             ));
@@ -2680,8 +2680,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               SELECT `domain` from `domain_admins`
                 WHERE (`active`='1' AND `username` = :username))
               )
-              OR ('admin'= :role)
-              AND `domain` != 'ALL'");
+              OR 'admin'= :role");
           $stmt->execute(array(
             ':username' => $_SESSION['mailcow_cc_username'],
             ':role' => $_SESSION['mailcow_cc_role'],

+ 41 - 31
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "03102018_1502";
+    $db_version = "07102018_1502";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -135,7 +135,6 @@ function init_db_schema() {
       ),
       "api" => array(
         "cols" => array(
-          "username" => "VARCHAR(255) NOT NULL",
           "api_key" => "VARCHAR(255) NOT NULL",
           "allow_from" => "VARCHAR(512) NOT NULL",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
@@ -144,16 +143,8 @@ function init_db_schema() {
         ),
         "keys" => array(
           "primary" => array(
-            "" => array("username")
+            "" => array("api_key")
           ),
-          "fkey" => array(
-            "fk_username_api" => array(
-              "col" => "username",
-              "ref" => "admin.username",
-              "delete" => "CASCADE",
-              "update" => "CASCADE"
-            )
-          )
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
@@ -448,14 +439,6 @@ function init_db_schema() {
         "keys" => array(
           "primary" => array(
             "" => array("username")
-          ),
-          "fkey" => array(
-            "fk_domain_admin_acl" => array(
-              "col" => "username",
-              "ref" => "domain_admins.username",
-              "delete" => "CASCADE",
-              "update" => "NO ACTION"
-            )
           )
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
@@ -987,22 +970,49 @@ DELIMITER ;';
           WHERE `username` = :username");
       $stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user));
     }
-    $_SESSION['return'][] = array(
-      'type' => 'success',
-      'log' => array(__FUNCTION__),
-      'msg' => 'db_init_complete'
-    );
-
+    if (php_sapi_name() == "cli") {
+      echo "DB initialization completed" . PHP_EOL;
+    } else {
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__),
+        'msg' => 'db_init_complete'
+      );
+    }
     // Fix ACL
     $stmt = $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);");
     $stmt = $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);");
+    // Fix domain_admins
+    $stmt = $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';");
   }
   catch (PDOException $e) {
-    $_SESSION['return'][] = array(
-      'type' => 'danger',
-      'log' => array(__FUNCTION__),
-      'msg' => array('mysql_error', $e)
-    );
+    if (php_sapi_name() == "cli") {
+      echo "DB initialization failed: " . print_r($e, true) . PHP_EOL;
+    } else {
+      $_SESSION['return'][] = array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__),
+        'msg' => array('mysql_error', $e)
+      );
+    }
   }
 }
-?>
+if (php_sapi_name() == "cli") {
+  include '/web/inc/vars.inc.php';
+  $now = new DateTime();
+  $mins = $now->getOffset() / 60;
+  $sgn = ($mins < 0 ? -1 : 1);
+  $mins = abs($mins);
+  $hrs = floor($mins / 60);
+  $mins -= $hrs * 60;
+  $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins);
+  $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+  $opt = [
+    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
+    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+    PDO::ATTR_EMULATE_PREPARES   => false,
+    PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
+  ];
+  $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
+  init_db_schema();
+}

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

@@ -139,6 +139,7 @@ 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.address_rewriting.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.admin.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantine.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';

+ 4 - 4
data/web/inc/sessions.inc.php

@@ -28,16 +28,16 @@ if (!isset($_SESSION['SESS_REMOTE_UA'])) {
 
 // API
 if (!empty($_SERVER['HTTP_X_API_KEY'])) {
-  $stmt = $pdo->prepare("SELECT `username`, `allow_from` FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
+  $stmt = $pdo->prepare("SELECT `allow_from` FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
   $stmt->execute(array(
-    ':api_key' => preg_replace('/[^A-Z0-9-]/i', '', $_SERVER['HTTP_X_API_KEY'])
+    ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
   ));
   $api_return = $stmt->fetch(PDO::FETCH_ASSOC);
   if (!empty($api_return['username'])) {
     $remote = get_remote_ip(false);
     $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
     if (in_array($remote, $allow_from)) {
-      $_SESSION['mailcow_cc_username'] = $api_return['username'];
+      $_SESSION['mailcow_cc_username'] = 'API';
       $_SESSION['mailcow_cc_role'] = 'admin';
       $_SESSION['mailcow_cc_api'] = true;
     }
@@ -84,7 +84,7 @@ if (isset($_POST["logout"])) {
     $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
     $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
     unset($_SESSION["dual-login"]);
-    header("Location: /mailbox.php");
+    header("Location: /mailbox");
     exit();
   }
   else {

+ 9 - 6
data/web/inc/triggers.inc.php

@@ -6,7 +6,7 @@ if (isset($_POST["verify_tfa_login"])) {
     unset($_SESSION['pending_mailcow_cc_username']);
     unset($_SESSION['pending_mailcow_cc_role']);
     unset($_SESSION['pending_tfa_method']);
-		header("Location: /user.php");
+		header("Location: /user");
   }
 }
 
@@ -17,19 +17,19 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "admin";
     $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
-		header("Location: /admin.php");
+		header("Location: /admin");
 	}
 	elseif ($as == "domainadmin") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "domainadmin";
     $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
-		header("Location: /mailbox.php");
+		header("Location: /mailbox");
 	}
 	elseif ($as == "user") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "user";
     $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
-		header("Location: /user.php");
+		header("Location: /user");
 	}
 	elseif ($as != "pending") {
     unset($_SESSION['pending_mailcow_cc_username']);
@@ -49,7 +49,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['acl']['login_as'] == "1")
         $_SESSION["dual-login"]["role"]     = $_SESSION['mailcow_cc_role'];
         $_SESSION['mailcow_cc_username']    = $duallogin;
         $_SESSION['mailcow_cc_role']        = "user";
-        header("Location: /user.php");
+        header("Location: /user");
       }
     }
     else {
@@ -58,7 +58,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['acl']['login_as'] == "1")
         $_SESSION["dual-login"]["role"]     = $_SESSION['mailcow_cc_role'];
         $_SESSION['mailcow_cc_username']    = $duallogin;
         $_SESSION['mailcow_cc_role']        = "domainadmin";
-        header("Location: /user.php");
+        header("Location: /user");
       }
     }
   }
@@ -93,5 +93,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
 	if (isset($_POST["rspamd_ui"])) {
 		rspamd_ui('edit', $_POST);
 	}
+	if (isset($_POST["mass_send"])) {
+		sys_mail($_POST);
+	}
 }
 ?>

+ 6 - 1
data/web/inc/vars.inc.php

@@ -87,7 +87,12 @@ $AVAILABLE_LANGUAGES = array('de', 'en', 'es', 'fr', 'lv', 'nl', 'pl', 'pt', 'ru
 $DEFAULT_THEME = 'lumen';
 
 // Password complexity as regular expression
-$PASSWD_REGEP = '.{4,}';
+// Min. 6 characters
+//$PASSWD_REGEP = '.{6,}';
+// Min. 6 characters, which must include at least one uppercase letter, one lowercase letter and one number
+// $PASSWD_REGEP = '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{6,}$';
+// Min. 6 characters, which must include at least one letter and one number
+$PASSWD_REGEP = '^(?=.*[0-9])(?=.*[A-Za-z]).{6,}$';
 
 // Show DKIM private keys - false by default
 $SHOW_DKIM_PRIV_KEYS = false;

+ 4 - 3
data/web/index.php

@@ -1,5 +1,5 @@
 <?php
-require_once 'inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
   header('Location: /admin');
@@ -13,7 +13,8 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   header('Location: /user');
   exit();
 }
-require_once 'inc/header.inc.php';
+
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 
 ?>
@@ -109,4 +110,4 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 </div><!-- /.container -->
 <script src="/js/index.js"></script>
 <?php
-require_once 'inc/footer.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

+ 51 - 1
data/web/js/admin.js

@@ -35,6 +35,15 @@ jQuery(function($){
      });
      $('#dkim_add_domains').val(domains);
   });
+  $("#mass_exclude").change(function(){ 
+    $("#mass_include").selectpicker('deselectAll');
+  });
+  $("#mass_include").change(function(){ 
+    $("#mass_exclude").selectpicker('deselectAll');
+  });
+  $("#mass_disarm").click(function() {
+    $("#mass_send").attr("disabled", !this.checked);
+  });
   function draw_domain_admins() {
     ft_domainadmins = FooTable.init('#domainadminstable', {
       "columns": [
@@ -63,6 +72,32 @@ jQuery(function($){
       "sorting": {"enabled": true}
     });
   }
+  function draw_admins() {
+    ft_admins = FooTable.init('#adminstable', {
+      "columns": [
+        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"sorted": true,"name":"usr","title":lang.username,"style":{"width":"250px"}},
+        {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}},
+        {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"250px","width":"250px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/admin/all',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw admin table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'adminstable');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": false},
+      "sorting": {"enabled": true}
+    });
+  }
   function draw_fwd_hosts() {
     ft_forwardinghoststable = FooTable.init('#forwardinghoststable', {
       "columns": [
@@ -141,7 +176,8 @@ jQuery(function($){
       });
     } else if (table == 'domainadminstable') {
       $.each(data, function (i, item) {
-        item.selected_domains = escapeHtml(item.selected_domains.toString().replace(/,/g, " "));
+        item.selected_domains = escapeHtml(item.selected_domains);
+        item.selected_domains = item.selected_domains.toString().replace(/,/g, "<br>");
         item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
         item.action = '<div class="btn-group">' +
           '<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
@@ -149,11 +185,25 @@ jQuery(function($){
           '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' +
           '</div>';
       });
+    } else if (table == 'adminstable') {
+      $.each(data, function (i, item) {
+        if (admin_username == item.username) {
+          item.usr = '→ ' + item.username;
+        } else {
+          item.usr = item.username;
+        }
+        item.chkbox = '<input type="checkbox" data-id="admins" name="multi_select" value="' + item.username + '" />';
+        item.action = '<div class="btn-group">' +
+          '<a href="/edit/admin/' + 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-admin" data-api-url="delete/admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '</div>';
+      });
     }
     return data
   };
   // Initial table drawings
   draw_domain_admins();
+  draw_admins();
   draw_fwd_hosts();
   draw_relayhosts();
   // Relayhost

+ 10 - 1
data/web/js/api.js

@@ -81,6 +81,11 @@ $(document).ready(function() {
     } else {
       api_reload_window = true;
     }
+    if (typeof $(this).data('api-reload-location') !== 'undefined') {
+      api_reload_location = $(this).data('api-reload-location');
+    } else {
+      api_reload_location = '#';
+    }
     // If clicked element #edit_selected is in a form with the same data-id as the button,
     // we merge all input fields by {"name":"value"} into api-attr
     if ($(this).closest("form").data('id') == id) {
@@ -151,7 +156,11 @@ $(document).ready(function() {
             response_obj = JSON.parse(response);
           }
           if (api_reload_window === true) {
-            window.location = window.location.href.split("#")[0];
+            if (api_reload_location != '#') {
+              window.location.replace(api_reload_location)
+            } else {
+              window.location = window.location.href.split("#")[0];
+            }
           }
         }
       });

+ 7 - 1
data/web/js/debug.js

@@ -513,7 +513,13 @@ jQuery(function($){
     } else if (table == 'general_syslog') {
       $.each(data, function (i, item) {
         if (item === null) { return true; }
-        item.message = escapeHtml(item.message);
+        if (item.message.match("^base64,")) {
+          item.message = atob(item.message.slice(7));
+          item.message = item.message.replace(/(?!^)acme-client:/g, '<br>acme-client:')
+          item.message = item.message.replace(/acme-client:/g, '<b>acme-client:</b>')
+        } else {
+          item.message = escapeHtml(item.message);
+        }
         var danger_class = ["emerg", "alert", "crit", "err"];
         var warning_class = ["warning", "warn"];
         var info_class = ["notice", "info", "debug"];

+ 34 - 3
data/web/json_api.php

@@ -144,6 +144,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "domain-admin":
             process_add_return(domain_admin('add', $attr));
           break;
+          case "admin":
+            process_add_return(admin('add', $attr));
+          break;
           case "syncjob":
             process_add_return(mailbox('add', 'syncjob', $attr));
           break;
@@ -857,6 +860,31 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               break;
             }
           break;
+          case "admin":
+            switch ($object) {
+              case "all":
+                $admins = admin('get');
+                if (!empty($admins)) {
+                  foreach ($admins as $admin) {
+                    if ($details = admin('details', $admin)) {
+                      $data[] = $details;
+                    }
+                    else {
+                      continue;
+                    }
+                  }
+                  process_get_return($data);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+
+              default:
+                process_get_return(admin('details', $object));
+              break;
+            }
+          break;
           case "u2f-registration":
             header('Content-Type: application/javascript');
             if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) {
@@ -984,6 +1012,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "domain-admin":
             process_delete_return(domain_admin('delete', array('username' => $items)));
           break;
+          case "admin":
+            process_delete_return(admin('delete', array('username' => $items)));
+          break;
         }
       break;
       case "edit":
@@ -1088,6 +1119,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "domain-admin":
             process_edit_return(domain_admin('edit', array_merge(array('username' => $items), $attr)));
           break;
+          case "admin":
+            process_edit_return(admin('edit', array_merge(array('username' => $items), $attr)));
+          break;
           case "fwdhost":
             process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
           break;
@@ -1104,9 +1138,6 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
             elseif ($_SESSION['mailcow_cc_role'] == "user") {
               process_edit_return(edit_user_account($attr));
             }
-            elseif ($_SESSION['mailcow_cc_role'] == "admin") {
-              process_edit_return(edit_admin_account($attr));
-            }
           break;
         }
       break;

+ 0 - 4
data/web/lang/lang.ca.php

@@ -204,8 +204,6 @@ $lang['mailbox']['target_address'] = 'Direcció Goto';
 $lang['mailbox']['username'] = "Nom d'usuari";
 $lang['mailbox']['fname'] = 'Nom complert';
 $lang['mailbox']['filter_table'] = 'Filtrar taula';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'En ús (%)';
 $lang['mailbox']['msg_num'] = 'Missatge #';
 $lang['mailbox']['remove'] = 'Esborrar';
@@ -406,8 +404,6 @@ $lang['admin']['save'] = 'Desar els canvis';
 $lang['admin']['admin'] = 'Administrador';
 $lang['admin']['admin_details'] = "Editar detalls de l'administrador";
 $lang['admin']['unchanged_if_empty'] = "Si no hi ha canvis, deixa'l en blanc";
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Accés';
 $lang['admin']['no_record'] = 'Cap registre';
 $lang['admin']['filter_table'] = 'Filtrar taula';

+ 19 - 5
data/web/lang/lang.de.php

@@ -72,7 +72,7 @@ $lang['success']['dkim_removed'] = 'DKIM-Key wurde entfernt';
 $lang['success']['dkim_added'] = 'DKIM-Key wurde hinzugefügt';
 $lang['success']['dkim_duplicated'] = "DKIM-Key der Domain %s wurde auf Domain %s kopiert";
 $lang['danger']['access_denied'] = 'Zugriff verweigert oder unvollständige/ungültige Daten';
-$lang['danger']['domain_invalid'] = 'Domainname %s ist ungültig';
+$lang['danger']['domain_invalid'] = 'Domainname ist leer oder ungültig';
 $lang['danger']['mailbox_quota_exceeds_domain_quota'] = 'Maximale Größe für Mailboxen überschreitet das Domain Speicherlimit';
 $lang['danger']['object_is_not_numeric'] = 'Wert %s ist nicht numerisch';
 $lang['success']['domain_added'] = 'Domain %s wurde angelegt';
@@ -105,7 +105,9 @@ $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden ges
 $lang['success']['domain_modified'] = 'Änderungen an Domain %s wurden gespeichert';
 $lang['success']['domain_admin_modified'] = 'Änderungen an Domain-Administrator %s wurden gespeichert';
 $lang['success']['domain_admin_added'] = 'Domain-Administrator %s wurde angelegt';
+$lang['success']['admin_added'] = 'Administrator %s wurde angelegt';
 $lang['success']['admin_modified'] = 'Änderungen am Administrator wurden gespeichert';
+$lang['success']['admin_api_modified'] = "Änderungen an API wurden gespeichert";
 $lang['danger']['username_invalid'] = 'Benutzername %s kann nicht verwendet werden';
 $lang['danger']['password_mismatch'] = 'Passwort-Wiederholung stimmt nicht überein';
 $lang['danger']['password_complexity'] = 'Passwort entspricht nicht den Richtlinien';
@@ -129,9 +131,12 @@ $lang['success']['domain_removed'] = 'Domain %s wurde entfernt';
 $lang['success']['alias_removed'] = 'Alias-Adresse %s wurde entfernt';
 $lang['success']['alias_domain_removed'] = 'Alias-Domain %s wurde entfernt';
 $lang['success']['domain_admin_removed'] = 'Domain-Administrator %s wurde entfernt';
+$lang['success']['admin_removed'] = 'Administrator %s wurde entfernt';
 $lang['success']['mailbox_removed'] = 'Mailbox %s wurde entfernt';
 $lang['success']['eas_reset'] = "ActiveSync Gerät des Benutzers %s wurden zurückgesetzt";
 $lang['success']['resource_removed'] = 'Ressource %s wurde entfernt';
+$lang['warning']['cannot_delete_self'] = 'Kann derzeit eingeloggten Benutzer nicht entfernen';
+$lang['warning']['no_active_admin'] = 'Kann letzten aktiven Administrator nicht deaktivieren';
 $lang['danger']['max_quota_in_use'] = 'Mailbox Speicherplatzlimit muss größer oder gleich %d MiB sein';
 $lang['danger']['domain_quota_m_in_use'] = 'Domain Speicherplatzlimit muss größer oder gleich %d MiB sein';
 $lang['danger']['mailboxes_in_use'] = 'Maximale Anzahl an Mailboxen muss größer oder gleich %d sein';
@@ -279,8 +284,6 @@ $lang['mailbox']['target_address'] = 'Ziel-Adresse';
 $lang['mailbox']['username'] = 'Benutzername';
 $lang['mailbox']['fname'] = 'Name';
 $lang['mailbox']['filter_table'] = 'Filtern';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'Prozentualer Gebrauch';
 $lang['mailbox']['msg_num'] = 'Anzahl Nachrichten';
 $lang['mailbox']['remove'] = 'Entfernen';
@@ -505,6 +508,7 @@ $lang['admin']['active'] = 'Aktiv';
 $lang['admin']['inactive'] = 'Inaktiv';
 $lang['admin']['action'] = 'Aktion';
 $lang['admin']['add_domain_admin'] = 'Domain-Administrator hinzufügen';
+$lang['admin']['domain_admin'] = 'Administrator hinzufügen';
 $lang['admin']['add_settings_rule'] = 'Rspamd Regel hinzufügen';
 $lang['admin']['rsetting_desc'] = 'Kurze Beschreibung';
 $lang['admin']['rsetting_content'] = 'Regelinhalt';
@@ -523,8 +527,6 @@ $lang['admin']['save'] = 'Änderungen speichern';
 $lang['admin']['admin'] = 'Administrator';
 $lang['admin']['admin_details'] = 'Administrator bearbeiten';
 $lang['admin']['unchanged_if_empty'] = 'Unverändert, wenn leer';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Zugang';
 $lang['admin']['no_record'] = 'Kein Eintrag';
 $lang['admin']['filter_table'] = 'Tabelle Filtern';
@@ -687,3 +689,15 @@ $lang['success']['tls_policy_map_entry_saved'] = 'TLS-Richtlinieneintrag "%s" wu
 $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";
+
+$lang['admin']['sys_mails'] = 'System mails';
+$lang['admin']['subject'] = 'Subject';
+$lang['admin']['from'] = 'From';
+$lang['admin']['include_exclude'] = 'Include/Exclude';
+$lang['admin']['include_exclude_info'] = 'By default - with no selection - <b>all mailboxes</b> are addressed';
+$lang['admin']['excludes'] = 'Excludes these recipients';
+$lang['admin']['includes'] = 'Include these recipients';
+$lang['admin']['text'] = 'Text';
+$lang['admin']['activate_send'] = 'Activate send button';
+
+$lang['warning']['ip_invalid'] = 'Ungültige IP übersprungen: %s';

+ 24 - 6
data/web/lang/lang.en.php

@@ -76,7 +76,7 @@ $lang['success']['dkim_removed'] = "DKIM key %s has been removed";
 $lang['success']['dkim_added'] = "DKIM key %s has been saved";
 $lang['success']['dkim_duplicated'] = "DKIM key for domain %s has been copied to %s";
 $lang['danger']['access_denied'] = "Access denied or invalid form data";
-$lang['danger']['domain_invalid'] = "Domain name %s is invalid";
+$lang['danger']['domain_invalid'] = "Domain name is empty or invalid";
 $lang['danger']['mailbox_quota_exceeds_domain_quota'] = "Max. quota exceeds domain quota limit";
 $lang['danger']['object_is_not_numeric'] = "Value %s is not numeric";
 $lang['success']['domain_added'] = "Added domain %s";
@@ -108,7 +108,9 @@ $lang['success']['aliasd_modified'] = "Changes to alias domain %s have been save
 $lang['success']['domain_modified'] = "Changes to domain %s have been saved";
 $lang['success']['domain_admin_modified'] = "Changes to domain administrator %s have been saved";
 $lang['success']['domain_admin_added'] = "Domain administrator %s has been added";
+$lang['success']['admin_added'] = "Administrator %s has been added";
 $lang['success']['admin_modified'] = "Changes to administrator have been saved";
+$lang['success']['admin_api_modified'] = "Changes to API have been saved";
 $lang['danger']['username_invalid'] = "Username %s cannot be used";
 $lang['danger']['password_mismatch'] = "Confirmation password does not match";
 $lang['danger']['password_complexity'] = "Password does not meet the policy";
@@ -132,9 +134,12 @@ $lang['success']['domain_removed'] = "Domain %s has been removed";
 $lang['success']['alias_removed'] = "Alias %s has been removed";
 $lang['success']['alias_domain_removed'] = "Alias domain %s has been removed";
 $lang['success']['domain_admin_removed'] = "Domain administrator %s has been removed";
+$lang['success']['admin_removed'] = "Administrator %s has been removed";
 $lang['success']['mailbox_removed'] = "Mailbox %s has been removed";
 $lang['success']['eas_reset'] = "ActiveSync devices for user %s were reset";
 $lang['success']['resource_removed'] = "Resource %s has been removed";
+$lang['warning']['cannot_delete_self'] = "Cannot delete logged in user";
+$lang['warning']['no_active_admin'] = "Cannot deactivate last active admin";
 $lang['danger']['max_quota_in_use'] = "Mailbox quota must be greater or equal to %d MiB";
 $lang['danger']['domain_quota_m_in_use'] = "Domain quota must be greater or equal to %s MiB";
 $lang['danger']['mailboxes_in_use'] = "Max. mailboxes must be greater or equal to %d";
@@ -281,8 +286,8 @@ $lang['mailbox']['target_address'] = 'Goto address';
 $lang['mailbox']['username'] = 'Username';
 $lang['mailbox']['fname'] = 'Full name';
 $lang['mailbox']['filter_table'] = 'Filter table';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
+$lang['mailbox']['yes'] = '&#10003;';
+$lang['mailbox']['no'] = '&#10005;';
 $lang['mailbox']['in_use'] = 'In use (%)';
 $lang['mailbox']['msg_num'] = 'Message #';
 $lang['mailbox']['remove'] = 'Remove';
@@ -517,6 +522,7 @@ $lang['admin']['active'] = 'Active';
 $lang['admin']['inactive'] = 'Inactive';
 $lang['admin']['action'] = 'Action';
 $lang['admin']['add_domain_admin'] = 'Add domain administrator';
+$lang['admin']['add_admin'] = 'Add administrator';
 $lang['admin']['add_settings_rule'] = 'Add settings rule';
 $lang['admin']['rsetting_desc'] = 'Short description';
 $lang['admin']['rsetting_content'] = 'Rule content';
@@ -535,8 +541,8 @@ $lang['admin']['save'] = 'Save changes';
 $lang['admin']['admin'] = 'Administrator';
 $lang['admin']['admin_details'] = 'Edit administrator details';
 $lang['admin']['unchanged_if_empty'] = 'If unchanged leave blank';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
+$lang['admin']['yes'] = '&#10003;';
+$lang['admin']['no'] = '&#10005;';
 $lang['admin']['access'] = 'Access';
 $lang['admin']['no_record'] = 'No record';
 $lang['admin']['filter_table'] = 'Filter table';
@@ -571,7 +577,7 @@ $lang['diagnostics']['cname_from_a'] = 'Value derived from A/AAAA record. This i
 
 $lang['admin']['relay_from'] = '"From:" address';
 $lang['admin']['relay_run'] = "Run test";
-$lang['admin']['api_allow_from'] = "Allow API access from these IPs";
+$lang['admin']['api_allow_from'] = "Allow API access from these IPs (separated by comma or new line)";
 $lang['admin']['api_key'] = "API key";
 $lang['admin']['activate_api'] = "Activate API";
 $lang['admin']['regen_api_key'] = "Regenerate API key";
@@ -707,3 +713,15 @@ $lang['oauth2']['permit'] = 'Authorize application';
 $lang['oauth2']['authorize_app'] = 'Authorize application';
 $lang['oauth2']['deny'] = 'Deny';
 $lang['oauth2']['access_denied'] = 'Please login as mailbox owner to grant access via OAuth2.';
+
+$lang['admin']['sys_mails'] = 'System mails';
+$lang['admin']['subject'] = 'Subject';
+$lang['admin']['from'] = 'From';
+$lang['admin']['include_exclude'] = 'Include/Exclude';
+$lang['admin']['include_exclude_info'] = 'By default - with no selection - <b>all mailboxes</b> are addressed';
+$lang['admin']['excludes'] = 'Excludes these recipients';
+$lang['admin']['includes'] = 'Include these recipients';
+$lang['admin']['text'] = 'Text';
+$lang['admin']['activate_send'] = 'Activate send button';
+
+$lang['warning']['ip_invalid'] = 'Skipped invalid IP: %s';

+ 0 - 4
data/web/lang/lang.es.php

@@ -153,8 +153,6 @@ $lang['mailbox']['target_address'] = 'Dirección Goto';
 $lang['mailbox']['username'] = 'Nombre de usuario';
 $lang['mailbox']['fname'] = 'Nombre completo';
 $lang['mailbox']['filter_table'] = 'Filtrar tabla';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'En uso (%)';
 $lang['mailbox']['msg_num'] = 'Mensaje #';
 $lang['mailbox']['remove'] = 'Eliminar';
@@ -259,7 +257,5 @@ $lang['admin']['save'] = 'Guardar cambios';
 $lang['admin']['admin'] = 'Administrador';
 $lang['admin']['admin_details'] = 'Editar detalles del administrador';
 $lang['admin']['unchanged_if_empty'] = 'Si no hay cambios dejalo en blanco';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Acceso';
 $lang['admin']['no_record'] = 'Sin registro';

+ 0 - 4
data/web/lang/lang.fr.php

@@ -188,8 +188,6 @@ $lang['mailbox']['target_address'] = "Adresse cible";
 $lang['mailbox']['username'] = "Identifiant";
 $lang['mailbox']['fname'] = "Nom complet";
 $lang['mailbox']['filter_table'] = "Table de filtrage";
-$lang['mailbox']['yes'] = "&#10004;";
-$lang['mailbox']['no'] = "&#10008;";
 $lang['mailbox']['in_use'] = "Utilisation (%)";
 $lang['mailbox']['msg_num'] = "Message";
 $lang['mailbox']['remove'] = "Retirer";
@@ -358,8 +356,6 @@ $lang['admin']['remove'] = "Retirer";
 $lang['admin']['admin'] = "Administrateur";
 $lang['admin']['admin_details'] = "Éditer les informations de l'administrateur";
 $lang['admin']['unchanged_if_empty'] = "Si aucun changement, laisser vide";
-$lang['admin']['yes'] = "&#10004;";
-$lang['admin']['no'] = "&#10008;";
 $lang['admin']['access'] = "Accès";
 $lang['admin']['no_record'] = "Aucun enregistrement";
 $lang['admin']['filter_table'] = "Table de filtrage";

+ 0 - 4
data/web/lang/lang.it.php

@@ -191,8 +191,6 @@ $lang['mailbox']['target_address'] = 'Vai ad indirizzo';
 $lang['mailbox']['username'] = 'Nome utente';
 $lang['mailbox']['fname'] = 'Nome completo';
 $lang['mailbox']['filter_table'] = 'Filra tabella';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'In uso (%)';
 $lang['mailbox']['msg_num'] = 'Messaggio #';
 $lang['mailbox']['remove'] = 'Rimuovi';
@@ -348,8 +346,6 @@ $lang['admin']['save'] = 'Salva modifiche';
 $lang['admin']['admin'] = 'Amministratore';
 $lang['admin']['admin_details'] = 'Modifica impostazioni amministratore';
 $lang['admin']['unchanged_if_empty'] = 'Se immutato lasciare vuoto';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Accedi';
 $lang['admin']['no_record'] = 'Nessun risultato';
 $lang['admin']['filter_table'] = 'Tabella filtro';

+ 0 - 4
data/web/lang/lang.lv.php

@@ -206,8 +206,6 @@ $lang['mailbox']['target_address'] = 'Doties uz  adresi';
 $lang['mailbox']['username'] = 'Lietotājvārds';
 $lang['mailbox']['fname'] = 'Pilns vārds';
 $lang['mailbox']['filter_table'] = 'Filtra tabula';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'Lietošanā (%)';
 $lang['mailbox']['msg_num'] = 'Vēstule #';
 $lang['mailbox']['remove'] = 'Noņemt';
@@ -404,8 +402,6 @@ $lang['admin']['save'] = 'Saglabāt izmaiņas';
 $lang['admin']['admin'] = 'Administrators';
 $lang['admin']['admin_details'] = 'Labot administratora detaļas';
 $lang['admin']['unchanged_if_empty'] = 'Ja nav veiktas izmaiņas, atstājiet tukšu';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Pieeja';
 $lang['admin']['no_record'] = 'Nav ierakstu';
 $lang['admin']['filter_table'] = 'Filtru tabula';

+ 0 - 4
data/web/lang/lang.nl.php

@@ -260,8 +260,6 @@ $lang['mailbox']['target_address'] = 'Doeladres';
 $lang['mailbox']['username'] = 'Gebruikersnaam';
 $lang['mailbox']['fname'] = 'Volledige naam';
 $lang['mailbox']['filter_table'] = 'Filtertabel';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'In gebruik (%)';
 $lang['mailbox']['msg_num'] = 'Bericht #';
 $lang['mailbox']['remove'] = 'Verwijder';
@@ -485,8 +483,6 @@ $lang['admin']['save'] = 'Sla wijzigingen op';
 $lang['admin']['admin'] = 'Beheerder';
 $lang['admin']['admin_details'] = 'Wijzig beheerderdetails';
 $lang['admin']['unchanged_if_empty'] = 'Laat leeg wanneer onveranderd';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Toegang';
 $lang['admin']['no_record'] = 'Geen vermelding';
 $lang['admin']['filter_table'] = 'Filtertabel';

+ 0 - 4
data/web/lang/lang.pl.php

@@ -193,8 +193,6 @@ $lang['mailbox']['target_address'] = 'Adres Idź do';
 $lang['mailbox']['username'] = 'Nazwa użytkownika';
 $lang['mailbox']['fname'] = 'Pełna nazwa';
 $lang['mailbox']['filter_table'] = 'Tabela filtru';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'W użyciu (%)';
 $lang['mailbox']['msg_num'] = 'Wiadomość #';
 $lang['mailbox']['remove'] = 'Usuń';
@@ -360,8 +358,6 @@ $lang['admin']['save'] = 'Zapisz zmiany';
 $lang['admin']['admin'] = 'Administrator';
 $lang['admin']['admin_details'] = 'Edytuj szczegóły administratora';
 $lang['admin']['unchanged_if_empty'] = 'W przypadku braku zmian, nie wypełniaj';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Dostęp';
 $lang['admin']['no_record'] = 'Brak rekordu';
 $lang['admin']['filter_table'] = 'Tabela filtru';

+ 0 - 4
data/web/lang/lang.pt.php

@@ -140,8 +140,6 @@ $lang['mailbox']['target_address'] = 'Encaminhar para';
 $lang['mailbox']['username'] = 'Usuário';
 $lang['mailbox']['fname'] = 'Nome';
 $lang['mailbox']['filter_table'] = 'Procurar';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'Em uso (%)';
 $lang['mailbox']['msg_num'] = 'Mensagens';
 $lang['mailbox']['remove'] = 'Remover';
@@ -239,7 +237,5 @@ $lang['admin']['save'] = 'Salvar';
 $lang['admin']['admin'] = 'Administrador';
 $lang['admin']['admin_details'] = 'Editar informações do administrator';
 $lang['admin']['unchanged_if_empty'] = 'Deixar em branco para não alterar';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Acessos';
 $lang['admin']['no_record'] = 'Nenhum registro';

+ 0 - 4
data/web/lang/lang.ru.php

@@ -189,8 +189,6 @@ $lang['mailbox']['target_address'] = 'Основной адрес';
 $lang['mailbox']['username'] = 'Имя пользователя';
 $lang['mailbox']['fname'] = 'Полное имя';
 $lang['mailbox']['filter_table'] = 'Поиск';
-$lang['mailbox']['yes'] = '&#10004;';
-$lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['in_use'] = 'Использовано (%)';
 $lang['mailbox']['msg_num'] = 'Письма #';
 $lang['mailbox']['remove'] = 'Удалить';
@@ -359,8 +357,6 @@ $lang['admin']['save'] = 'Сохранить изменения';
 $lang['admin']['admin'] = 'Администратор';
 $lang['admin']['admin_details'] = 'Изменить данные администратора';
 $lang['admin']['unchanged_if_empty'] = 'Если без изменений оставьте пустым';
-$lang['admin']['yes'] = '&#10004;';
-$lang['admin']['no'] = '&#10008;';
 $lang['admin']['access'] = 'Доступ к';
 $lang['admin']['no_record'] = 'Нет записей';
 $lang['admin']['filter_table'] = 'Поиск';

+ 4 - 4
data/web/mailbox.php

@@ -1,8 +1,7 @@
 <?php
-require_once "inc/prerequisites.inc.php";
-
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
-require_once "inc/header.inc.php";
+require_once $_SERVER['DOCUMENT_ROOT'] .  '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 ?>
 <div class="container">
@@ -340,7 +339,8 @@ echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 <script src="/js/mailbox.js"></script>
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
-} else {
+}
+else {
 	header('Location: /');
 	exit();
 }

+ 1 - 1
data/web/mobileconfig.php

@@ -1,5 +1,5 @@
 <?php
-require_once 'inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 
 if (empty($mailcow_hostname)) {
   exit();

+ 46 - 0
data/web/modals/admin.php

@@ -105,6 +105,52 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
   </div>
 </div><!-- add domain admin modal -->
+<!-- add admin modal -->
+<div class="modal fade" id="addAdminModal" 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['admin']['add_admin'];?></h3>
+      </div>
+      <div class="modal-body">
+          <form class="form-horizontal" data-cached-form="true" data-id="add_admin" role="form" method="post">
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="username"><?=$lang['admin']['username'];?>:</label>
+              <div class="col-sm-10">
+                <input type="text" class="form-control" name="username" required>
+                &rdsh; <kbd>a-z A-Z - _ .</kbd>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="password"><?=$lang['admin']['password'];?>:</label>
+              <div class="col-sm-10">
+              <input type="password" class="form-control" data-hibp="true" name="password" placeholder="" required>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="password2"><?=$lang['admin']['password_repeat'];?>:</label>
+              <div class="col-sm-10">
+              <input type="password" class="form-control" name="password2" placeholder="" required>
+              </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['admin']['active'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <button class="btn btn-default" data-action="add_item" data-id="add_admin" data-api-url='add/admin' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <?=$lang['admin']['add'];?></button>
+              </div>
+            </div>
+          </form>
+      </div>
+    </div>
+  </div>
+</div><!-- add admin modal -->
 <!-- test relayhost modal -->
 <div class="modal fade" id="testRelayhostModal" tabindex="-1" role="dialog" aria-hidden="true">
   <div class="modal-dialog modal-lg">

+ 2 - 2
data/web/quarantine.php

@@ -1,8 +1,8 @@
 <?php
-require_once "inc/prerequisites.inc.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 
 if (isset($_SESSION['mailcow_cc_role'])) {
-require_once "inc/header.inc.php";
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 
 ?>

+ 4 - 4
data/web/user.php

@@ -1,12 +1,12 @@
 <?php
-require_once("inc/prerequisites.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
 
   /*
   / DOMAIN ADMIN
   */
 
-	require_once("inc/header.inc.php");
+	require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 	$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
   $tfa_data = get_tfa();
 	$username = $_SESSION['mailcow_cc_username'];
@@ -71,7 +71,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   / USER
   */
 
-  require_once("inc/header.inc.php");
+  require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
   $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
   $username = $_SESSION['mailcow_cc_username'];
   $mailboxdata = mailbox('get', 'mailbox_details', $username);
@@ -410,7 +410,7 @@ echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 <script src="/js/footable.min.js"></script>
 <script src="/js/user.js"></script>
 <?php
-require_once("inc/footer.inc.php");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
 }
 else {
 	header('Location: /');