Browse Source

Make alias domains selectable in sender acl, a lot of code changes, added challenges for u2f to json_api, added U2F as TFA

andryyy 8 years ago
parent
commit
badef73191

+ 244 - 227
data/web/admin.php

@@ -6,208 +6,192 @@ require_once("inc/header.inc.php");
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 ?>
 ?>
 <div class="container">
 <div class="container">
-<h4><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?=$lang['admin']['access'];?></h4>
+  <h4><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?=$lang['admin']['access'];?></h4>
 
 
-<div class="panel-group" id="accordion_access">
-	<div class="panel panel-danger">
-		<div class="panel-heading"><?=$lang['admin']['admin_details'];?></div>
-		<div class="panel-body">
-			<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
-			<?php $admindetails = get_admin_details(); ?>
-				<input type="hidden" name="admin_user_now" value="<?=htmlspecialchars($admindetails['username']);?>">
-				<div class="form-group">
-					<label class="control-label col-sm-2" for="admin_user"><?=$lang['admin']['admin'];?>:</label>
-					<div class="col-sm-10">
-						<input type="text" class="form-control" name="admin_user" id="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-2" for="admin_pass"><?=$lang['admin']['password'];?>:</label>
-					<div class="col-sm-10">
-					<input type="password" class="form-control" name="admin_pass" id="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
-					</div>
-				</div>
-				<div class="form-group">
-					<label class="control-label col-sm-2" for="admin_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
-					<div class="col-sm-10">
-					<input type="password" class="form-control" name="admin_pass2" id="admin_pass2">
-					</div>
-				</div>
-				<div class="form-group">
-					<div class="col-sm-offset-2 col-sm-10">
-						<button type="submit" name="set_admin_account" class="btn btn-default"><?=$lang['admin']['save'];?></button>
-					</div>
-				</div>
-			</form>
-		</div>
-	</div>
-	<div class="panel panel-default">
-	<div style="cursor:pointer;" class="panel-heading" data-toggle="collapse" data-parent="#accordion_access" data-target="#collapseDomAdmins">
-		<span class="accordion-toggle"><?=$lang['admin']['domain_admins'];?></span>
-	</div>
-		<div id="collapseDomAdmins" class="panel-collapse collapse">
-			<div class="panel-body">
-				<form method="post">
-					<div class="table-responsive">
-					<table class="table table-striped sortable-theme-bootstrap" data-sortable id="domainadminstable">
-						<thead>
-						<tr>
-							<th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
-							<th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
-							<th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
-							<th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th>
-						</tr>
-						</thead>
-						<tbody>
-							<?php
-              foreach (get_domain_admins() as $domain_admin) {
-                $da_data = get_domain_admin_details($domain_admin); 
-                if (!empty($da_data)):
-							?>
-							<tr id="data">
-								<td><?=htmlspecialchars(strtolower($domain_admin));?></td>
-								<td>
-								<?php
-								foreach ($da_data['selected_domains'] as $domain) {
-									echo htmlspecialchars($domain).'<br />';
-								}
-								?>
-								</td>
-								<td><?=$da_data['active'];?></td>
-								<td style="text-align: right;">
-									<div class="btn-group">
-										<a href="edit.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['admin']['edit'];?></a>
-										<a href="delete.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['admin']['remove'];?></a>
-									</div>
-								</td>
-								</td>
-							</tr>
-
-							<?php
-							else:
-							?>
-								<tr id="no-data"><td colspan="4" style="text-align: center; font-style: italic;"><?=$lang['admin']['no_record'];?></td></tr>
-							<?php
-							endif;
-              }
-							?>
-						</tbody>
-					</table>
-					</div>
-				</form>
-				<small>
-				<legend><?=$lang['admin']['add_domain_admin'];?></legend>
-				<form class="form-horizontal" 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" id="username" required>
-							&rdsh; <kbd>a-z A-Z - _ .</kbd>
-						</div>
-					</div>
-					<div class="form-group">
-						<label class="control-label col-sm-2" for="name"><?=$lang['admin']['admin_domains'];?>:</label>
-						<div class="col-sm-10">
-							<select title="<?=$lang['admin']['search_domain_da'];?>" style="width:100%" name="domain[]" size="5" multiple>
-							<?php
-							foreach (mailbox_get_domains() as $domain) {
-								echo "<option>".htmlspecialchars($domain)."</option>";
-							}
-							?>
-							</select>
-						</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" name="password" id="password" placeholder="">
-						</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" id="password2" placeholder="">
-						</div>
-					</div>
-					<div class="form-group">
-						<div class="col-sm-offset-2 col-sm-10">
-							<div class="checkbox">
-							<label><input type="checkbox" name="active" checked> <?=$lang['admin']['active'];?></label>
-							</div>
-						</div>
-					</div>
-					<div class="form-group">
-						<div class="col-sm-offset-2 col-sm-10">
-							<button type="submit" name="add_domain_admin" class="btn btn-default"><?=$lang['admin']['add'];?></button>
-						</div>
-					</div>
-				</form>
-				</small>
-			</div>
-		</div>
-	</div>
-</div>
-
-<h4><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> <?=$lang['admin']['configuration'];?></h4>
-<div class="panel panel-default">
-<div class="panel-heading"><?=$lang['admin']['dkim_keys'];?></div>
-<div id="collapseDKIM" class="panel-collapse">
-<div class="panel-body">
-  <p style="margin-bottom:40px"><?=$lang['admin']['dkim_key_hint'];?></p>
-	<?php
-	foreach(mailbox_get_domains() as $domain) {
-      if (!empty($dkim = dkim_get_key_details($domain))) {
-    ?>
-      <div class="row">
-        <div class="col-xs-3">
-          <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br />
-            <span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span>
-            <span class="label label-info"><?=$dkim['length'];?> bit</span>
-          </p>
+  <div class="panel-group" id="accordion_access">
+    <div class="panel panel-danger">
+      <div class="panel-heading"><?=$lang['admin']['admin_details'];?></div>
+      <div class="panel-body">
+        <form class="form-horizontal" autocapitalize="none" 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" id="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" class="form-control" name="admin_pass" id="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" id="admin_pass2">
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <button type="submit" name="edit_admin_account" class="btn btn-default"><?=$lang['admin']['save'];?></button>
+            </div>
+          </div>
+        </form>
+        <hr>
+        <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">
+            <p><?=get_tfa()['pretty'];?></p>
+          </div>
         </div>
         </div>
-        <div class="col-xs-8">
-            <pre><?=$dkim['dkim_txt'];?></pre>
+        <div class="row">
+          <div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
+          <div class="col-md-9 col-xs-7">
+            <select data-width="auto" 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="none"><?=$lang['tfa']['none'];?></option>
+            </select>
+          </div>
         </div>
         </div>
-        <div class="col-xs-1">
-          <form class="form-inline" method="post">
-            <input type="hidden" name="domain" value="<?=$domain;?>">
-            <input type="hidden" name="dkim_delete_key" value="1">
-              <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+      </div>
+    </div>
+    <div class="panel panel-default">
+    <div style="cursor:pointer;" class="panel-heading" data-toggle="collapse" data-parent="#accordion_access" data-target="#collapseDomAdmins">
+      <span class="accordion-toggle"><?=$lang['admin']['domain_admins'];?></span>
+    </div>
+      <div id="collapseDomAdmins" class="panel-collapse collapse">
+        <div class="panel-body">
+          <form method="post">
+            <div class="table-responsive">
+            <table class="table table-striped sortable-theme-bootstrap" data-sortable id="domainadminstable">
+              <thead>
+              <tr>
+                <th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
+                <th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
+                <th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
+                <th class="sort-table" style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th>
+                <th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th>
+              </tr>
+              </thead>
+              <tbody>
+                <?php
+                foreach (get_domain_admins() as $domain_admin) {
+                  $da_data = get_domain_admin_details($domain_admin); 
+                  if (!empty($da_data)):
+                ?>
+                <tr id="data">
+                  <td><?=htmlspecialchars(strtolower($domain_admin));?></td>
+                  <td>
+                  <?php
+                  foreach ($da_data['selected_domains'] as $domain) {
+                    echo htmlspecialchars($domain).'<br />';
+                  }
+                  ?>
+                  </td>
+                  <td><?=$da_data['active'];?></td>
+                  <td><?=empty($da_data['tfa_active_int']) ? "✘" : "✔";?></td>
+                  <td style="text-align: right;">
+                    <div class="btn-group">
+                      <a href="edit.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['admin']['edit'];?></a>
+                      <a href="delete.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['admin']['remove'];?></a>
+                    </div>
+                  </td>
+                  </td>
+                </tr>
+
+                <?php
+                else:
+                ?>
+                  <tr id="no-data"><td colspan="4" style="text-align: center; font-style: italic;"><?=$lang['admin']['no_record'];?></td></tr>
+                <?php
+                endif;
+                }
+                ?>
+              </tbody>
+            </table>
+            </div>
           </form>
           </form>
+          <small>
+          <legend><?=$lang['admin']['add_domain_admin'];?></legend>
+          <form class="form-horizontal" 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" id="username" required>
+                &rdsh; <kbd>a-z A-Z - _ .</kbd>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="name"><?=$lang['admin']['admin_domains'];?>:</label>
+              <div class="col-sm-10">
+                <select title="<?=$lang['admin']['search_domain_da'];?>" style="width:100%" name="domain[]" size="5" multiple>
+                <?php
+                foreach (mailbox_get_domains() as $domain) {
+                  echo "<option>".htmlspecialchars($domain)."</option>";
+                }
+                ?>
+                </select>
+              </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" name="password" id="password" placeholder="">
+              </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" id="password2" placeholder="">
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <div class="checkbox">
+                <label><input type="checkbox" name="active" checked> <?=$lang['admin']['active'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <button type="submit" name="add_domain_admin" class="btn btn-default"><?=$lang['admin']['add'];?></button>
+              </div>
+            </div>
+          </form>
+          </small>
         </div>
         </div>
       </div>
       </div>
-    <?php
-    }
-    else {
-    ?>
-    <div class="row">
-      <div class="col-xs-3">
-        <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
-      </div>
-      <div class="col-xs-8"><pre>-</pre></div>
-      <div class="col-xs-1">&nbsp;</div>
     </div>
     </div>
+  </div>
+
+  <h4><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> <?=$lang['admin']['configuration'];?></h4>
+  <div class="panel panel-default">
+  <div class="panel-heading"><?=$lang['admin']['dkim_keys'];?></div>
+  <div id="collapseDKIM" class="panel-collapse">
+  <div class="panel-body">
+    <p style="margin-bottom:40px"><?=$lang['admin']['dkim_key_hint'];?></p>
     <?php
     <?php
-    }
-    foreach(mailbox_get_alias_domains($domain) as $alias_domain) {
-      if (!empty($dkim = dkim_get_key_details($alias_domain))) {
+    foreach(mailbox_get_domains() as $domain) {
+        if (!empty($dkim = dkim_get_key_details($domain))) {
       ?>
       ?>
         <div class="row">
         <div class="row">
-          <div class="col-xs-offset-1 col-xs-2">
-            <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small>
+          <div class="col-xs-3">
+            <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br />
               <span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span>
               <span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span>
               <span class="label label-info"><?=$dkim['length'];?> bit</span>
               <span class="label label-info"><?=$dkim['length'];?> bit</span>
-          </p>
+            </p>
           </div>
           </div>
           <div class="col-xs-8">
           <div class="col-xs-8">
-            <pre><?=$dkim['dkim_txt'];?></pre>
+              <pre><?=$dkim['dkim_txt'];?></pre>
           </div>
           </div>
           <div class="col-xs-1">
           <div class="col-xs-1">
             <form class="form-inline" method="post">
             <form class="form-inline" method="post">
-              <input type="hidden" name="domain" value="<?=$alias_domain;?>">
+              <input type="hidden" name="domain" value="<?=$domain;?>">
               <input type="hidden" name="dkim_delete_key" value="1">
               <input type="hidden" name="dkim_delete_key" value="1">
-              <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
             </form>
             </form>
           </div>
           </div>
         </div>
         </div>
@@ -216,57 +200,90 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
       else {
       else {
       ?>
       ?>
       <div class="row">
       <div class="row">
-        <div class="col-xs-2 col-xs-offset-1">
-          <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
+        <div class="col-xs-3">
+          <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
         </div>
         </div>
-      <div class="col-xs-8"><pre>-</pre></div>
-      <div class="col-xs-1">&nbsp;</div>
+        <div class="col-xs-8"><pre>-</pre></div>
+        <div class="col-xs-1">&nbsp;</div>
       </div>
       </div>
       <?php
       <?php
       }
       }
-    }
-	}
-  foreach(dkim_get_blind_keys() as $blind) {
-    if (!empty($dkim = dkim_get_key_details($blind))) {
-    ?>
-      <div class="row">
-        <div class="col-xs-3">
-          <p>Domain: <strong><?=htmlspecialchars($blind);?></strong><br /><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
-        </div>
-          <div class="col-xs-8">
-            <pre><?=$dkim['dkim_txt'];?></pre>
+      foreach(mailbox_get_alias_domains($domain) as $alias_domain) {
+        if (!empty($dkim = dkim_get_key_details($alias_domain))) {
+        ?>
+          <div class="row">
+            <div class="col-xs-offset-1 col-xs-2">
+              <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small>
+                <span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span>
+                <span class="label label-info"><?=$dkim['length'];?> bit</span>
+            </p>
+            </div>
+            <div class="col-xs-8">
+              <pre><?=$dkim['dkim_txt'];?></pre>
+            </div>
+            <div class="col-xs-1">
+              <form class="form-inline" method="post">
+                <input type="hidden" name="domain" value="<?=$alias_domain;?>">
+                <input type="hidden" name="dkim_delete_key" value="1">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+              </form>
+            </div>
           </div>
           </div>
-          <div class="col-xs-1">
-            <form class="form-inline" method="post">
-              <input type="hidden" name="domain" value="<?=$blind;?>">
-              <input type="hidden" name="dkim_delete_key" value="1">
-              <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
-            </form>
+        <?php
+        }
+        else {
+        ?>
+        <div class="row">
+          <div class="col-xs-2 col-xs-offset-1">
+            <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
           </div>
           </div>
-      </div>
-    <?php
+        <div class="col-xs-8"><pre>-</pre></div>
+        <div class="col-xs-1">&nbsp;</div>
+        </div>
+        <?php
+        }
+      }
+    }
+    foreach(dkim_get_blind_keys() as $blind) {
+      if (!empty($dkim = dkim_get_key_details($blind))) {
+      ?>
+        <div class="row">
+          <div class="col-xs-3">
+            <p>Domain: <strong><?=htmlspecialchars($blind);?></strong><br /><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
+          </div>
+            <div class="col-xs-8">
+              <pre><?=$dkim['dkim_txt'];?></pre>
+            </div>
+            <div class="col-xs-1">
+              <form class="form-inline" method="post">
+                <input type="hidden" name="domain" value="<?=$blind;?>">
+                <input type="hidden" name="dkim_delete_key" value="1">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+              </form>
+            </div>
+        </div>
+      <?php
+      }
     }
     }
-  }
-  ?>
-	<legend style="margin-top:40px"><?=$lang['admin']['dkim_add_key'];?></legend>
-	<form class="form-inline" role="form" method="post">
-		<div class="form-group">
-			<label for="domain">Domain</label>
-			<input class="form-control" id="domain" name="domain" placeholder="example.org" required>
-		</div>
-		<div class="form-group">
-			<select data-width="200px" class="form-control" id="key_size" name="key_size" title="<?=$lang['admin']['dkim_key_length'];?>" required>
-				<option data-subtext="bits">1024</option>
-				<option data-subtext="bits">2048</option>
-			</select>
-		</div>
-		<button type="submit" name="dkim_add_key" class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
-	</form>
-</div>
-</div>
-</div>
+    ?>
+    <legend style="margin-top:40px"><?=$lang['admin']['dkim_add_key'];?></legend>
+    <form class="form-inline" role="form" method="post">
+      <div class="form-group">
+        <label for="domain">Domain</label>
+        <input class="form-control" id="domain" name="domain" placeholder="example.org" required>
+      </div>
+      <div class="form-group">
+        <select data-width="200px" class="form-control" id="key_size" name="key_size" title="<?=$lang['admin']['dkim_key_length'];?>" required>
+          <option data-subtext="bits">1024</option>
+          <option data-subtext="bits">2048</option>
+        </select>
+      </div>
+      <button type="submit" name="dkim_add_key" class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
+    </form>
+  </div>
+  </div>
+  </div>
 </div> <!-- /container -->
 </div> <!-- /container -->
-
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script>
 <script src="js/sorttable.js"></script>
 <script src="js/sorttable.js"></script>
 <script src="js/admin.js"></script>
 <script src="js/admin.js"></script>

+ 11 - 4
data/web/edit.php

@@ -66,11 +66,11 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 				<h4><?=$lang['edit']['domain_admin'];?></h4>
 				<h4><?=$lang['edit']['domain_admin'];?></h4>
 				<br />
 				<br />
 				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
 				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
-				<input type="hidden" name="username" value="<?=htmlspecialchars($domain_admin);?>">
+				<input type="hidden" name="username_now" value="<?=htmlspecialchars($domain_admin);?>">
 					<div class="form-group">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="username"><?=$lang['edit']['username'];?></label>
 						<label class="control-label col-sm-2" for="username"><?=$lang['edit']['username'];?></label>
 						<div class="col-sm-10">
 						<div class="col-sm-10">
-              <input class="form-control" type="text" disabled value="<?=htmlspecialchars($domain_admin);?>" />
+              <input class="form-control" type="text" name="username" value="<?=htmlspecialchars($domain_admin);?>" />
 						</div>
 						</div>
 					</div>
 					</div>
 					<div class="form-group">
 					<div class="form-group">
@@ -113,7 +113,14 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 					</div>
 					</div>
 					<div class="form-group">
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 						<div class="col-sm-offset-2 col-sm-10">
-							<button type="submit" name="trigger_edit_domain_admin" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+							<div class="checkbox">
+							<label><input type="checkbox" name="delete_tfa"> <?=$lang['tfa']['delete_tfa'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="edit_domain_admin" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
 						</div>
 						</div>
 					</div>
 					</div>
 				</form>
 				</form>
@@ -575,7 +582,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
 					</div>
 					</div>
 					<div class="form-group">
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 						<div class="col-sm-offset-2 col-sm-10">
-							<button type="submit" name="trigger_edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+							<button type="submit" name="edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
 						</div>
 						</div>
 					</div>
 					</div>
 				</form>
 				</form>

BIN
data/web/img/yubi.ico


+ 0 - 146
data/web/inc/admin.inc.php

@@ -1,146 +0,0 @@
-<?php
-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') {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  try {
-    $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin`WHERE `superadmin`='1' AND active='1'");
-    $stmt->execute();
-    $data = $stmt->fetch(PDO::FETCH_ASSOC);
-  }
-  catch(PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-  }
-  return $data;
-}
-function edit_admin($postarray) {
-	global $lang;
-	global $pdo;
-	$username     = $postarray['username'];
-	$password     = $postarray['password'];
-	$password2    = $postarray['password2'];
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	
-  if(isset($postarray['domain'])) {
-    foreach ($postarray['domain'] as $domain) {
-      if (!is_valid_domain_name($domain)) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => sprintf($lang['danger']['domain_invalid'])
-        );
-        return false;
-      }
-    }
-	}
-
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
-		$stmt->execute(array(
-			':username' => $username,
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-  if(isset($postarray['domain'])) {
-    foreach ($postarray['domain'] as $domain) {
-      try {
-        $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
-          VALUES (:username, :domain, :created, :active)");
-        $stmt->execute(array(
-          ':username' => $username,
-          ':domain' => $domain,
-          ':created' => date('Y-m-d H:i:s'),
-          ':active' => $active
-        ));
-      }
-      catch (PDOException $e) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => 'MySQL: '.$e
-        );
-        return false;
-      }
-    }
-	}
-
-	if (!empty($password) && !empty($password2)) {
-		if ($password != $password2) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['password_mismatch'])
-			);
-			return false;
-		}
-		$password_hashed = hash_password($password);
-		try {
-			$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
-			$stmt->execute(array(
-				':password_hashed' => $password_hashed,
-				':username' => $username,
-				':modified' => date('Y-m-d H:i:s'),
-				':active' => $active
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	else {
-		try {
-			$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active WHERE `username` = :username");
-			$stmt->execute(array(
-				':username' => $username,
-				':modified' => date('Y-m-d H:i:s'),
-				':active' => $active
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
-	);
-}

+ 0 - 142
data/web/inc/dkim.inc.php

@@ -1,142 +0,0 @@
-<?php
-function dkim_add_key($postarray) {
-	global $lang;
-	global $pdo;
-  if ($_SESSION['mailcow_cc_role'] != "admin") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  // if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-    // $_SESSION['return'] = array(
-      // 'type' => 'danger',
-      // 'msg' => sprintf($lang['danger']['access_denied'])
-    // );
-    // return false;
-  // }
-  $key_length	= intval($postarray['key_size']);
-  $domain	= $postarray['domain'];
-  if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
-    );
-    return false;
-  }
-
-  if (!empty(glob($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'))) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
-    );
-    return false;
-  }
-
-  $config = array(
-    "digest_alg" => "sha256",
-    "private_key_bits" => $key_length,
-    "private_key_type" => OPENSSL_KEYTYPE_RSA,
-  );
-  if ($keypair_ressource = openssl_pkey_new($config)) {
-    $key_details = openssl_pkey_get_details($keypair_ressource);
-    $pubKey = implode(array_slice(
-        array_filter(
-          explode(PHP_EOL, $key_details['key'])
-        ), 1, -1)
-      );
-    // Save public key to file
-    file_put_contents($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim', $pubKey);
-    // Save private key to file
-    openssl_pkey_export_to_file($keypair_ressource, $GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim');
-    $_SESSION['return'] = array(
-      'type' => 'success',
-      'msg' => sprintf($lang['success']['dkim_added'])
-    );
-    return true;
-  }
-  else {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
-    );
-    return false;
-  }
-}
-function dkim_get_key_details($domain) {
-  $data = array();
-  if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-    $dkim_pubkey_file = escapeshellarg($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
-    if (file_exists(substr($dkim_pubkey_file, 1, -1))) {
-      $data['pubkey'] = file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
-      $data['length'] = (strlen($data['pubkey']) < 391) ? 1024 : 2048;
-      $data['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
-    }
-  }
-  return $data;
-}
-function dkim_get_blind_keys() {
-	global $lang;
-  if ($_SESSION['mailcow_cc_role'] != "admin") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  $domains = array();
-  $dnstxt_folder = scandir($GLOBALS["MC_DKIM_TXTS"]);
-  $dnstxt_files = array_diff($dnstxt_folder, array('.', '..'));
-  foreach($dnstxt_files as $file) {
-    $domains[] = substr($file, 0, -5);
-  }
-  return array_diff($domains, array_merge(mailbox_get_domains(), mailbox_get_alias_domains()));
-}
-function dkim_delete_key($postarray) {
-	global $lang;
-  $domain	= $postarray['domain'];
-
-  if ($_SESSION['mailcow_cc_role'] != "admin") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  // if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-    // $_SESSION['return'] = array(
-      // 'type' => 'danger',
-      // 'msg' => sprintf($lang['danger']['access_denied'])
-    // );
-    // return false;
-  // }
-  if (!is_valid_domain_name($domain)) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
-    );
-    return false;
-  }
-  exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'), $out, $return);
-  if ($return != "0") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['dkim_remove_failed'])
-    );
-    return false;
-  }
-  exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim'), $out, $return);
-  if ($return != "0") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['dkim_remove_failed'])
-    );
-    return false;
-  }
-  $_SESSION['return'] = array(
-    'type' => 'success',
-    'msg' => sprintf($lang['success']['dkim_removed'])
-  );
-  return true;
-}

+ 0 - 381
data/web/inc/domainadmin.inc.php

@@ -1,381 +0,0 @@
-<?php
-function add_domain_admin($postarray) {
-	global $lang;
-	global $pdo;
-	$username		= strtolower(trim($postarray['username']));
-	$password		= $postarray['password'];
-	$password2		= $postarray['password2'];
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	if (empty($postarray['domain'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_invalid'])
-		);
-		return false;
-	}
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-	try {
-		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
-			WHERE `username` = :username");
-		$stmt->execute(array(':username' => $username));
-		$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-		
-		$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));
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	foreach ($num_results as $num_results_each) {
-		if ($num_results_each != 0) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
-			);
-			return false;
-		}
-	}
-	if (!empty($password) && !empty($password2)) {
-		if ($password != $password2) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['password_mismatch'])
-			);
-			return false;
-		}
-		$password_hashed = hash_password($password);
-		foreach ($postarray['domain'] as $domain) {
-			if (!is_valid_domain_name($domain)) {
-				$_SESSION['return'] = array(
-					'type' => 'danger',
-					'msg' => sprintf($lang['danger']['domain_invalid'])
-				);
-				return false;
-			}
-			try {
-				$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
-						VALUES (:username, :domain, :created, :active)");
-				$stmt->execute(array(
-					':username' => $username,
-					':domain' => $domain,
-					':created' => date('Y-m-d H:i:s'),
-					':active' => $active
-				));
-			}
-			catch (PDOException $e) {
-        delete_domain_admin(array('username' => $username));
-				$_SESSION['return'] = array(
-					'type' => 'danger',
-					'msg' => 'MySQL: '.$e
-				);
-				return false;
-			}
-		}
-		try {
-			$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`)
-				VALUES (:username, :password_hashed, '0', :created, :modified, :active)");
-			$stmt->execute(array(
-				':username' => $username,
-				':password_hashed' => $password_hashed,
-				':created' => date('Y-m-d H:i:s'),
-				':modified' => date('Y-m-d H:i:s'),
-				':active' => $active
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	else {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['password_empty'])
-		);
-		return false;
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username))
-	);
-}
-function delete_domain_admin($postarray) {
-	global $pdo;
-	global $lang;
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	$username = $postarray['username'];
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
-		$stmt->execute(array(
-			':username' => $username,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
-		$stmt->execute(array(
-			':username' => $username,
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars($username))
-	);
-}
-function get_domain_admins() {
-	global $pdo;
-	global $lang;
-  $domainadmins = array();
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-  try {
-    $stmt = $pdo->query("SELECT DISTINCT
-      `username`
-        FROM `domain_admins` 
-          WHERE `username` IN (
-            SELECT `username` FROM `admin`
-              WHERE `superadmin`!='1'
-          )");
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while ($row = array_shift($rows)) {
-      $domainadmins[] = $row['username'];
-    }
-  }
-  catch(PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-  }
-  return $domainadmins;
-}
-function get_domain_admin_details($domain_admin) {
-	global $pdo;
-	global $lang;
-  $domainadmindata = array();
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-  if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $domain_admin))) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-  try {
-    $stmt = $pdo->prepare("SELECT
-      `created`,
-      `active` AS `active_int`,
-      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
-        FROM `domain_admins`
-          WHERE `username`= :domain_admin");
-    $stmt->execute(array(
-      ':domain_admin' => $domain_admin
-    ));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
-    $domainadmindata['active'] = $row['active'];
-    $domainadmindata['active_int'] = $row['active_int'];
-    $domainadmindata['created'] = $row['created'];
-    // GET SELECTED
-    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
-      WHERE `domain` IN (
-        SELECT `domain` FROM `domain_admins`
-          WHERE `username`= :domain_admin)");
-    $stmt->execute(array(':domain_admin' => $domain_admin));
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while($row = array_shift($rows)) {
-      $domainadmindata['selected_domains'][] = $row['domain'];
-    }
-    // GET UNSELECTED
-    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
-      WHERE `domain` NOT IN (
-        SELECT `domain` FROM `domain_admins`
-          WHERE `username`= :domain_admin)");
-    $stmt->execute(array(':domain_admin' => $domain_admin));
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while($row = array_shift($rows)) {
-      $domainadmindata['unselected_domains'][] = $row['domain'];
-    }
-  }
-  catch(PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-  }
-  return $domainadmindata;
-}
-function edit_domain_admin($postarray) {
-	global $lang;
-	global $pdo;
-	$username     = $postarray['username'];
-	$password     = $postarray['password'];
-	$password2    = $postarray['password2'];
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	
-  if(isset($postarray['domain'])) {
-    foreach ($postarray['domain'] as $domain) {
-      if (!is_valid_domain_name($domain)) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => sprintf($lang['danger']['domain_invalid'])
-        );
-        return false;
-      }
-    }
-	}
-
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
-		$stmt->execute(array(
-			':username' => $username,
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-  if(isset($postarray['domain'])) {
-    foreach ($postarray['domain'] as $domain) {
-      try {
-        $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
-          VALUES (:username, :domain, :created, :active)");
-        $stmt->execute(array(
-          ':username' => $username,
-          ':domain' => $domain,
-          ':created' => date('Y-m-d H:i:s'),
-          ':active' => $active
-        ));
-      }
-      catch (PDOException $e) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => 'MySQL: '.$e
-        );
-        return false;
-      }
-    }
-	}
-
-	if (!empty($password) && !empty($password2)) {
-		if ($password != $password2) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['password_mismatch'])
-			);
-			return false;
-		}
-		$password_hashed = hash_password($password);
-		try {
-			$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
-			$stmt->execute(array(
-				':password_hashed' => $password_hashed,
-				':username' => $username,
-				':modified' => date('Y-m-d H:i:s'),
-				':active' => $active
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	else {
-		try {
-			$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active WHERE `username` = :username");
-			$stmt->execute(array(
-				':username' => $username,
-				':modified' => date('Y-m-d H:i:s'),
-				':active' => $active
-			));
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
-	);
-}

+ 86 - 0
data/web/inc/footer.inc.php

@@ -1,4 +1,5 @@
 <?php
 <?php
+include("inc/tfa_modals.php");
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin"):
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin"):
 ?>
 ?>
 <div id="RestartSOGo" class="modal fade" role="dialog">
 <div id="RestartSOGo" class="modal fade" role="dialog">
@@ -26,6 +27,7 @@ endif;
 <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-switch/3.3.2/js/bootstrap-switch.min.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-switch/3.3.2/js/bootstrap-switch.min.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/7.0.2/bootstrap-slider.min.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/7.0.2/bootstrap-slider.min.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.4/js/bootstrap-select.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.4/js/bootstrap-select.js"></script>
+<script src="/js/u2f-api.js"></script>
 <script>
 <script>
 // Select language and reopen active URL without POST
 // Select language and reopen active URL without POST
 function setLang(sel) {
 function setLang(sel) {
@@ -34,6 +36,90 @@ function setLang(sel) {
 }
 }
 
 
 $(document).ready(function() {
 $(document).ready(function() {
+  // Confirm TFA modal
+  <?php if (isset($_SESSION['pending_tfa_method'])):?>
+  $('#ConfirmTFAModal').modal({
+    backdrop: 'static',
+    keyboard: false
+  }); 
+  $('#ConfirmTFAModal').on('shown.bs.modal', function(){
+      $(this).find('#token').focus();
+      // If U2F
+      if(document.getElementById("u2f_auth_data") !== null) {
+        $.ajax({
+          type: "GET",
+          cache: false,
+          dataType: 'script',
+          url: "json_api.php",
+          data: {
+            'action':'get_u2f_auth_challenge',
+            'object':'<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>',
+          },
+          success: function(data){
+            data;
+          }
+        });
+        setTimeout(function() {
+          console.log("sign: ", req);
+          u2f.sign(req, function(data) {
+            var form = document.getElementById('u2f_auth_form');
+            var auth = document.getElementById('u2f_auth_data');
+            console.log("Authenticate callback", data);
+            auth.value = JSON.stringify(data);
+            form.submit();
+          });
+        }, 1000);
+      }
+  });
+  <?php endif; ?>
+
+  // Set TFA modals
+  $('#selectTFA').change(function () {
+    if ($(this).val() == "yubi_otp") {
+      $('#YubiOTPModal').modal('show');
+      $("option:selected").prop("selected", false);
+    }
+    if ($(this).val() == "u2f") {
+      $('#U2FModal').modal('show');
+      $("option:selected").prop("selected", false);
+      $.ajax({
+        type: "GET",
+        cache: false,
+        dataType: 'script',
+        url: "json_api.php",
+        data: {
+          'action':'get_u2f_reg_challenge',
+          'object':'<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>',
+        },
+        success: function(data){
+          data;
+        }
+      });
+      setTimeout(function() {
+        console.log("Register: ", req);
+        u2f.register([req], sigs, function(data) {
+          var form  = document.getElementById('u2f_reg_form');
+          var reg   = document.getElementById('u2f_register_data');
+          console.log("Register callback", data);
+          if (data.errorCode && data.errorCode != 0) {
+            var u2f_return_code = document.getElementById('u2f_return_code');
+            u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
+            if (data.errorCode == "4") { data.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; }
+            u2f_return_code.innerHTML = 'Error code: ' + data.errorCode;
+            return;
+          }
+          reg.value = JSON.stringify(data);
+          form.submit();
+        });
+      }, 1000);
+    }
+    if ($(this).val() == "none") {
+      $('#DisableTFAModal').modal('show');
+      $("option:selected").prop("selected", false);
+    }
+  });
+
+  // Activate tooltips
   $(function () {
   $(function () {
     $('[data-toggle="tooltip"]').tooltip()
     $('[data-toggle="tooltip"]').tooltip()
   })
   })

+ 2937 - 49
data/web/inc/functions.inc.php

@@ -1,8 +1,4 @@
 <?php
 <?php
-require_once 'dkim.inc.php';
-require_once 'mailbox.inc.php';
-require_once 'domainadmin.inc.php';
-require_once 'admin.inc.php';
 function hash_password($password) {
 function hash_password($password) {
 	$salt_str = bin2hex(openssl_random_pseudo_bytes(8));
 	$salt_str = bin2hex(openssl_random_pseudo_bytes(8));
 	return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
 	return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
@@ -69,7 +65,7 @@ function hasMailboxObjectAccess($username, $role, $object) {
 function init_db_schema() {
 function init_db_schema() {
 	global $pdo;
 	global $pdo;
 	try {
 	try {
-		$stmt = $pdo->prepare("SELECT NULL FROM `admin`, `imapsync`");
+		$stmt = $pdo->prepare("SELECT NULL FROM `admin`, `imapsync`, `tfa`");
 		$stmt->execute();
 		$stmt->execute();
 	}
 	}
 	catch (Exception $e) {
 	catch (Exception $e) {
@@ -171,9 +167,18 @@ function check_login($user, $pass) {
 	$stmt->execute(array(':user' => $user));
 	$stmt->execute(array(':user' => $user));
 	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 	foreach ($rows as $row) {
 	foreach ($rows as $row) {
-		if (verify_ssha256($row['password'], $pass) !== false) {
-			unset($_SESSION['ldelay']);
-			return "admin";
+		if (verify_ssha256($row['password'], $pass)) {
+      if (get_tfa($user)['name'] != "none") {
+        $_SESSION['pending_mailcow_cc_username'] = $user;
+        $_SESSION['pending_mailcow_cc_role'] = "admin";
+        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
+        unset($_SESSION['ldelay']);
+        return "pending";
+      }
+      else {
+        unset($_SESSION['ldelay']);
+        return "admin";
+      }
 		}
 		}
 	}
 	}
 	$stmt = $pdo->prepare("SELECT `password` FROM `admin`
 	$stmt = $pdo->prepare("SELECT `password` FROM `admin`
@@ -184,8 +189,17 @@ function check_login($user, $pass) {
 	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 	foreach ($rows as $row) {
 	foreach ($rows as $row) {
 		if (verify_ssha256($row['password'], $pass) !== false) {
 		if (verify_ssha256($row['password'], $pass) !== false) {
-			unset($_SESSION['ldelay']);
-			return "domainadmin";
+      if (get_tfa($user)['name'] != "none") {
+        $_SESSION['pending_mailcow_cc_username'] = $user;
+        $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
+        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
+        unset($_SESSION['ldelay']);
+        return "pending";
+      }
+      else {
+        unset($_SESSION['ldelay']);
+        return "domainadmin";
+      }
 		}
 		}
 	}
 	}
 	$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
 	$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
@@ -218,7 +232,7 @@ function formatBytes($size, $precision = 2) {
 	}
 	}
 	return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
 	return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
 }
 }
-function set_admin_account($postarray) {
+function edit_admin_account($postarray) {
 	global $lang;
 	global $lang;
 	global $pdo;
 	global $pdo;
 	if ($_SESSION['mailcow_cc_role'] != "admin") {
 	if ($_SESSION['mailcow_cc_role'] != "admin") {
@@ -228,23 +242,17 @@ function set_admin_account($postarray) {
 		);
 		);
 		return false;
 		return false;
 	}
 	}
-	$name		= $postarray['admin_user'];
-	$name_now	= $postarray['admin_user_now'];
+	$username       = $postarray['admin_user'];
+	$username_now   = $_SESSION['mailcow_cc_username'];
 
 
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $name)) || empty ($name)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $name_now)) || empty ($name_now)) {
+	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
 		$_SESSION['return'] = array(
 		$_SESSION['return'] = array(
 			'type' => 'danger',
 			'type' => 'danger',
 			'msg' => sprintf($lang['danger']['username_invalid'])
 			'msg' => sprintf($lang['danger']['username_invalid'])
 		);
 		);
 		return false;
 		return false;
 	}
 	}
+
 	if (!empty($postarray['admin_pass']) && !empty($postarray['admin_pass2'])) {
 	if (!empty($postarray['admin_pass']) && !empty($postarray['admin_pass2'])) {
 		if ($postarray['admin_pass'] != $postarray['admin_pass2']) {
 		if ($postarray['admin_pass'] != $postarray['admin_pass2']) {
 			$_SESSION['return'] = array(
 			$_SESSION['return'] = array(
@@ -258,13 +266,13 @@ function set_admin_account($postarray) {
 			$stmt = $pdo->prepare("UPDATE `admin` SET 
 			$stmt = $pdo->prepare("UPDATE `admin` SET 
 				`modified` = :modified,
 				`modified` = :modified,
 				`password` = :password_hashed,
 				`password` = :password_hashed,
-				`username` = :name
-					WHERE `username` = :username");
+				`username` = :username1
+					WHERE `username` = :username2");
 			$stmt->execute(array(
 			$stmt->execute(array(
 				':password_hashed' => $password_hashed,
 				':password_hashed' => $password_hashed,
 				':modified' => date('Y-m-d H:i:s'),
 				':modified' => date('Y-m-d H:i:s'),
-				':name' => $name,
-				':username' => $name_now
+				':username1' => $username,
+				':username2' => $username_now
 			));
 			));
 		}
 		}
 		catch (PDOException $e) {
 		catch (PDOException $e) {
@@ -279,12 +287,12 @@ function set_admin_account($postarray) {
 		try {
 		try {
 			$stmt = $pdo->prepare("UPDATE `admin` SET 
 			$stmt = $pdo->prepare("UPDATE `admin` SET 
 				`modified` = :modified,
 				`modified` = :modified,
-				`username` = :name
-					WHERE `username` = :name_now");
+				`username` = :username1
+					WHERE `username` = :username2");
 			$stmt->execute(array(
 			$stmt->execute(array(
-				':name' => $name,
+				':username1' => $username,
 				':modified' => date('Y-m-d H:i:s'),
 				':modified' => date('Y-m-d H:i:s'),
-				':name_now' => $name_now
+				':username2' => $username_now
 			));
 			));
 		}
 		}
 		catch (PDOException $e) {
 		catch (PDOException $e) {
@@ -296,15 +304,10 @@ function set_admin_account($postarray) {
 		}
 		}
 	}
 	}
 	try {
 	try {
-		$stmt = $pdo->prepare("UPDATE `domain_admins` SET 
-			`domain` = :domain,
-			`username` = :name
-				WHERE `username` = :name_now");
-		$stmt->execute(array(
-			':domain' => 'ALL',
-			':name' => $name,
-			':name_now' => $name_now
-		));
+		$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) {
 	catch (PDOException $e) {
 		$_SESSION['return'] = array(
 		$_SESSION['return'] = array(
@@ -313,6 +316,7 @@ function set_admin_account($postarray) {
 		);
 		);
 		return false;
 		return false;
 	}
 	}
+  $_SESSION['mailcow_cc_username'] = $username;
 	$_SESSION['return'] = array(
 	$_SESSION['return'] = array(
 		'type' => 'success',
 		'type' => 'success',
 		'msg' => sprintf($lang['success']['admin_modified'])
 		'msg' => sprintf($lang['success']['admin_modified'])
@@ -354,7 +358,7 @@ function set_time_limited_aliases($postarray) {
     return false;
     return false;
   }
   }
 
 
-	switch ($postarray["trigger_set_time_limited_aliases"]) {
+	switch ($postarray["set_time_limited_aliases"]) {
 		case "generate":
 		case "generate":
 			if (!is_numeric($postarray["validity"]) || $postarray["validity"] > 672) {
 			if (!is_numeric($postarray["validity"]) || $postarray["validity"] > 672) {
 				$_SESSION['return'] = array(
 				$_SESSION['return'] = array(
@@ -544,20 +548,23 @@ function edit_user_account($postarray) {
     $username = $_SESSION['mailcow_cc_username'];
     $username = $_SESSION['mailcow_cc_username'];
   }
   }
 	$password_old		= $postarray['user_old_pass'];
 	$password_old		= $postarray['user_old_pass'];
-	isset($postarray['togglePwNew']) ? $pwnew_active = '1' : $pwnew_active = '0';
 
 
-	if (isset($pwnew_active) && $pwnew_active == "1") {
+	if (isset($postarray['user_new_pass']) && isset($postarray['user_new_pass2'])) {
 		$password_new	= $postarray['user_new_pass'];
 		$password_new	= $postarray['user_new_pass'];
 		$password_new2	= $postarray['user_new_pass2'];
 		$password_new2	= $postarray['user_new_pass2'];
 	}
 	}
 
 
-	if (!check_login($username, $password_old) == "user") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
+	$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+			WHERE `username` = :user");
+	$stmt->execute(array(':user' => $username));
+	$row = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (!verify_ssha256($row['password'], $password_old)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
 
 
 	if (isset($password_new) && isset($password_new2)) {
 	if (isset($password_new) && isset($password_new2)) {
 		if (!empty($password_new2) && !empty($password_new)) {
 		if (!empty($password_new2) && !empty($password_new)) {
@@ -597,7 +604,7 @@ function edit_user_account($postarray) {
 	}
 	}
 	$_SESSION['return'] = array(
 	$_SESSION['return'] = array(
 		'type' => 'success',
 		'type' => 'success',
-		'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+		'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
 	);
 	);
 }
 }
 function get_spam_score($username = null) {
 function get_spam_score($username = null) {
@@ -1493,4 +1500,2885 @@ function is_valid_domain_name($domain_name) {
 		   && preg_match("/^.{1,253}$/", $domain_name)
 		   && preg_match("/^.{1,253}$/", $domain_name)
 		   && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
 		   && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
 }
 }
+function add_domain_admin($postarray) {
+	global $lang;
+	global $pdo;
+	$username		= strtolower(trim($postarray['username']));
+	$password		= $postarray['password'];
+	$password2		= $postarray['password2'];
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (empty($postarray['domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
+			WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+		
+		$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));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	foreach ($num_results as $num_results_each) {
+		if ($num_results_each != 0) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
+			);
+			return false;
+		}
+	}
+	if (!empty($password) && !empty($password2)) {
+		if ($password != $password2) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($password);
+		foreach ($postarray['domain'] as $domain) {
+			if (!is_valid_domain_name($domain)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['domain_invalid'])
+				);
+				return false;
+			}
+			try {
+				$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+						VALUES (:username, :domain, :created, :active)");
+				$stmt->execute(array(
+					':username' => $username,
+					':domain' => $domain,
+					':created' => date('Y-m-d H:i:s'),
+					':active' => $active
+				));
+			}
+			catch (PDOException $e) {
+        delete_domain_admin(array('username' => $username));
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+		}
+		try {
+			$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`)
+				VALUES (:username, :password_hashed, '0', :created, :modified, :active)");
+			$stmt->execute(array(
+				':username' => $username,
+				':password_hashed' => $password_hashed,
+				':created' => date('Y-m-d H:i:s'),
+				':modified' => date('Y-m-d H:i:s'),
+				':active' => $active
+			));
+		}
+		catch (PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	else {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['password_empty'])
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username))
+	);
+}
+function delete_domain_admin($postarray) {
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	$username = $postarray['username'];
+	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username,
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars($username))
+	);
+}
+function get_domain_admins() {
+	global $pdo;
+	global $lang;
+  $domainadmins = array();
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  try {
+    $stmt = $pdo->query("SELECT DISTINCT
+      `username`
+        FROM `domain_admins` 
+          WHERE `username` IN (
+            SELECT `username` FROM `admin`
+              WHERE `superadmin`!='1'
+          )");
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($rows)) {
+      $domainadmins[] = $row['username'];
+    }
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $domainadmins;
+}
+function get_domain_admin_details($domain_admin) {
+	global $pdo;
+	global $lang;
+  $domainadmindata = array();
+	if (isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  if (!isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  (!isset($domain_admin)) ? $domain_admin = $_SESSION['mailcow_cc_username'] : null;
+  
+  if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $domain_admin))) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+  try {
+    $stmt = $pdo->prepare("SELECT
+      `tfa`.`active` AS `tfa_active_int`,
+      `domain_admins`.`username`,
+      `domain_admins`.`created`,
+      `domain_admins`.`active` AS `active_int`,
+      CASE `domain_admins`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+        FROM `domain_admins`
+        LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
+          WHERE `domain_admins`.`username`= :domain_admin");
+    $stmt->execute(array(
+      ':domain_admin' => $domain_admin
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $domainadmindata['username'] = $row['username'];
+    $domainadmindata['active'] = $row['active'];
+    $domainadmindata['active_int'] = $row['active_int'];
+    $domainadmindata['tfa_active_int'] = $row['tfa_active_int'];
+    $domainadmindata['created'] = $row['created'];
+    // GET SELECTED
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` IN (
+        SELECT `domain` FROM `domain_admins`
+          WHERE `username`= :domain_admin)");
+    $stmt->execute(array(':domain_admin' => $domain_admin));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $domainadmindata['selected_domains'][] = $row['domain'];
+    }
+    // GET UNSELECTED
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` NOT IN (
+        SELECT `domain` FROM `domain_admins`
+          WHERE `username`= :domain_admin)");
+    $stmt->execute(array(':domain_admin' => $domain_admin));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $domainadmindata['unselected_domains'][] = $row['domain'];
+    }
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $domainadmindata;
+}
+function set_tfa($postarray) {
+	global $lang;
+	global $pdo;
+	global $yubi;
+	global $u2f;
+
+  if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
+    $_SESSION['mailcow_cc_role'] != "admin") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+  }
+  $username = $_SESSION['mailcow_cc_username'];
+  
+  $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+      WHERE `username` = :user");
+  $stmt->execute(array(':user' => $username));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (!verify_ssha256($row['password'], $postarray["confirm_password"])) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  
+	switch ($postarray["tfa_method"]) {
+		case "yubi_otp":
+			if (!ctype_alnum($postarray["otp_token"]) || strlen($postarray["otp_token"]) != 44) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['tfa_token_invalid'])
+				);
+				return false;
+			}
+      $yauth = $yubi->verify($postarray["otp_token"]);
+      if (PEAR::isError($yauth)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'Yubico Authentication error: ' . $yauth->getMessage()
+				);
+				return false;
+      }
+			try {
+				$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
+				$stmt->execute(array(':username' => $username));
+				$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `authmech`, `active`) VALUES
+					(:username, 'yubi_otp', 1)");
+				$stmt->execute(array(':username' => $username));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars($username))
+			);
+		break;
+
+		case "u2f":
+      try {
+        $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($postarray['token']));
+        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`) values (?, 'u2f', ?, ?, ?, ?)");
+        $stmt->execute(array($username, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
+        $_SESSION['return'] = array(
+          'type' => 'success',
+          'msg' => sprintf($lang['success']['object_modified'], $username)
+        );
+        $_SESSION['regReq'] = null;
+      }
+      catch (Exception $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => "U2F: " . $e->getMessage()
+        );
+        $_SESSION['regReq'] = null;
+      }
+		break;
+
+		case "none":
+			try {
+				$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+				$stmt->execute(array(':username' => $username));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars($username))
+			);
+		break;
+	}
+}
+function get_tfa($username = null) {
+	global $pdo;
+  if (isset($_SESSION['mailcow_cc_username'])) {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  elseif (empty($username)) {
+    return false;
+  }
+
+  $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
+      WHERE `username` = :username");
+  $stmt->execute(array(':username' => $username));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  
+	switch ($row["authmech"]) {
+		case "yubi_otp":
+      $data['name'] = "yubi_otp";
+      $data['pretty'] = "Yubico OTP";
+      return $data;
+    break;
+		case "u2f":
+      $data['name'] = "u2f";
+      $data['pretty'] = "Fido U2F";
+      return $data;
+    break;
+		case "hotp":
+      $data['name'] = "hotp";
+      $data['pretty'] = "HMAC-based OTP";
+      return $data;
+		break;
+ 		case "totp":
+      $data['name'] = "totp";
+      $data['pretty'] = "Time-based OTP";
+      return $data;
+		break;
+    default:
+      $data['name'] = 'none';
+      $data['pretty'] = "-";
+      return $data;
+    break;
+	}
+}
+function verify_tfa_login($username, $token) {
+	global $pdo;
+	global $lang;
+	global $yubi;
+
+  $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
+      WHERE `username` = :username");
+  $stmt->execute(array(':username' => $username));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  
+	switch ($row["authmech"]) {
+		case "yubi_otp":
+			if (!ctype_alnum($token) || strlen($token) != 44) {
+        return false;
+      }
+      $yauth = $yubi->verify($token);
+      if (PEAR::isError($yauth)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'Yubico Authentication error: ' . $yauth->getMessage()
+				);
+				return false;
+      }
+      else {
+        return true;
+      }
+    return false;
+  break;
+  case "u2f":
+    try {
+      global $u2f;
+      $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));
+      $stmt = $pdo->prepare("UPDATE `tfa` SET `counter` = ? WHERE `id` = ?");
+      $stmt->execute(array($reg->counter, $reg->id));
+      $_SESSION['authReq'] = null;
+      return true;
+    }
+    catch (Exception $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => "U2F: " . $e->getMessage()
+      );
+      $_SESSION['regReq'] = null;
+      return false;
+    }
+    return false;
+  break;
+  case "hotp":
+      return false;
+  break;
+  case "totp":
+      return false;
+  break;
+  default:
+      return false;
+  break;
+	}
+  return false;
+}
+function edit_domain_admin($postarray) {
+	global $lang;
+	global $pdo;
+
+	if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	// Administrator
+  if ($_SESSION['mailcow_cc_role'] == "admin") {
+    $username     = $postarray['username'];
+    $username_now = $postarray['username_now'];
+    $password     = $postarray['password'];
+    $password2    = $postarray['password2'];
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+
+    if(isset($postarray['domain'])) {
+      foreach ($postarray['domain'] as $domain) {
+        if (!is_valid_domain_name($domain)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['domain_invalid'])
+          );
+          return false;
+        }
+      }
+    }
+
+    if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['username_invalid'])
+      );
+      return false;
+    }
+    if ($username != $username_now) {
+      if (empty(get_domain_admin_details($username_now)['username']) || !empty(get_domain_admin_details($username)['username'])) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['username_invalid'])
+        );
+        return false;
+      }
+    }
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+      $stmt->execute(array(
+        ':username' => $username_now,
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+
+    if(isset($postarray['domain'])) {
+      foreach ($postarray['domain'] as $domain) {
+        try {
+          $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+            VALUES (:username, :domain, :created, :active)");
+          $stmt->execute(array(
+            ':username' => $username,
+            ':domain' => $domain,
+            ':created' => date('Y-m-d H:i:s'),
+            ':active' => $active
+          ));
+        }
+        catch (PDOException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+          return false;
+        }
+      }
+    }
+
+    if (!empty($password) && !empty($password2)) {
+      if ($password != $password2) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['password_mismatch'])
+        );
+        return false;
+      }
+      $password_hashed = hash_password($password);
+      try {
+        $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username2");
+        $stmt->execute(array(
+          ':password_hashed' => $password_hashed,
+          ':username1' => $username,
+          ':username2' => $username_now,
+          ':modified' => date('Y-m-d H:i:s'),
+          ':active' => $active
+        ));
+        if (isset($postarray['delete_tfa'])) {
+          $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+          $stmt->execute(array(':username' => $username_now));
+        }
+        else {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username WHERE `username` = :username_now");
+          $stmt->execute(array(':username' => $username, ':username_now' => $username_now));
+        }
+      }
+      catch (PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+    }
+    else {
+      try {
+        $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active WHERE `username` = :username2");
+        $stmt->execute(array(
+          ':username1' => $username,
+          ':username2' => $username_now,
+          ':modified' => date('Y-m-d H:i:s'),
+          ':active' => $active
+        ));
+        if (isset($postarray['delete_tfa'])) {
+          $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+          $stmt->execute(array(':username' => $username));
+        }
+        else {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username WHERE `username` = :username_now");
+          $stmt->execute(array(':username' => $username, ':username_now' => $username_now));
+        }
+      }
+      catch (PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+    }
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
+    );
+  }
+  // Domain administrator
+  // Can only edit itself
+  elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
+    $username = $_SESSION['mailcow_cc_username'];
+    $password_old		= $postarray['user_old_pass'];
+    $password_new	= $postarray['user_new_pass'];
+    $password_new2	= $postarray['user_new_pass2'];
+
+    $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+        WHERE `username` = :user");
+    $stmt->execute(array(':user' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (!verify_ssha256($row['password'], $password_old)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+
+    if (!empty($password_new2) && !empty($password_new)) {
+      if ($password_new2 != $password_new) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['password_mismatch'])
+        );
+        return false;
+      }
+      if (strlen($password_new) < "6" ||
+        !preg_match('/[A-Za-z]/', $password_new) ||
+        !preg_match('/[0-9]/', $password_new)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['password_complexity'])
+          );
+          return false;
+      }
+      $password_hashed = hash_password($password_new);
+      try {
+        $stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username");
+        $stmt->execute(array(
+          ':password_hashed' => $password_hashed,
+          ':modified' => date('Y-m-d H:i:s'),
+          ':username' => $username
+        ));
+      }
+      catch (PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+    }
+    
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
+    );
+  }
+}
+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') {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin`WHERE `superadmin`='1' AND active='1'");
+    $stmt->execute();
+    $data = $stmt->fetch(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $data;
+}
+function dkim_add_key($postarray) {
+	global $lang;
+	global $pdo;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  // if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    // $_SESSION['return'] = array(
+      // 'type' => 'danger',
+      // 'msg' => sprintf($lang['danger']['access_denied'])
+    // );
+    // return false;
+  // }
+  $key_length	= intval($postarray['key_size']);
+  $domain	= $postarray['domain'];
+  if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+
+  if (!empty(glob($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+
+  $config = array(
+    "digest_alg" => "sha256",
+    "private_key_bits" => $key_length,
+    "private_key_type" => OPENSSL_KEYTYPE_RSA,
+  );
+  if ($keypair_ressource = openssl_pkey_new($config)) {
+    $key_details = openssl_pkey_get_details($keypair_ressource);
+    $pubKey = implode(array_slice(
+        array_filter(
+          explode(PHP_EOL, $key_details['key'])
+        ), 1, -1)
+      );
+    // Save public key to file
+    file_put_contents($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim', $pubKey);
+    // Save private key to file
+    openssl_pkey_export_to_file($keypair_ressource, $GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim');
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['dkim_added'])
+    );
+    return true;
+  }
+  else {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+}
+function dkim_get_key_details($domain) {
+  $data = array();
+  if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    $dkim_pubkey_file = escapeshellarg($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
+    if (file_exists(substr($dkim_pubkey_file, 1, -1))) {
+      $data['pubkey'] = file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
+      $data['length'] = (strlen($data['pubkey']) < 391) ? 1024 : 2048;
+      $data['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
+    }
+  }
+  return $data;
+}
+function dkim_get_blind_keys() {
+	global $lang;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  $domains = array();
+  $dnstxt_folder = scandir($GLOBALS["MC_DKIM_TXTS"]);
+  $dnstxt_files = array_diff($dnstxt_folder, array('.', '..'));
+  foreach($dnstxt_files as $file) {
+    $domains[] = substr($file, 0, -5);
+  }
+  return array_diff($domains, array_merge(mailbox_get_domains(), mailbox_get_alias_domains()));
+}
+function dkim_delete_key($postarray) {
+	global $lang;
+  $domain	= $postarray['domain'];
+
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  // if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    // $_SESSION['return'] = array(
+      // 'type' => 'danger',
+      // 'msg' => sprintf($lang['danger']['access_denied'])
+    // );
+    // return false;
+  // }
+  if (!is_valid_domain_name($domain)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+  exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'), $out, $return);
+  if ($return != "0") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_remove_failed'])
+    );
+    return false;
+  }
+  exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim'), $out, $return);
+  if ($return != "0") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_remove_failed'])
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['dkim_removed'])
+  );
+  return true;
+}
+function mailbox_add_domain($postarray) {
+  // Array elements
+  // domain                 string
+  // description            string
+  // aliases                int
+  // mailboxes              int
+  // maxquota               int
+  // quota                  int
+  // active                 int
+  // relay_all_recipients   int
+  // backupmx               int
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	$domain				= idn_to_ascii(strtolower(trim($postarray['domain'])));
+	$description  = $postarray['description'];
+	$aliases			= $postarray['aliases'];
+	$mailboxes    = $postarray['mailboxes'];
+	$maxquota			= $postarray['maxquota'];
+	$quota				= $postarray['quota'];
+
+	if ($maxquota > $quota) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_exceeds_domain_quota'])
+		);
+		return false;
+	}
+
+	isset($postarray['active'])               ? $active = '1'                 : $active = '0';
+	isset($postarray['relay_all_recipients'])	? $relay_all_recipients = '1'   : $relay_all_recipients = '0';
+	isset($postarray['backupmx'])             ? $backupmx = '1'               : $backupmx = '0';
+	isset($postarray['relay_all_recipients']) ? $backupmx = '1'               : true;
+
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	foreach (array($quota, $maxquota, $mailboxes, $aliases) as $data) {
+		if (!is_numeric($data)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['object_is_not_numeric'], htmlspecialchars($data))
+			);
+			return false;
+		}
+	}
+
+	try {
+		$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));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if ($num_results != 0) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_exists'], htmlspecialchars($domain))
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `created`, `modified`, `active`, `relay_all_recipients`)
+			VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :created, :modified, :active, :relay_all_recipients)");
+		$stmt->execute(array(
+			':domain' => $domain,
+			':description' => $description,
+			':aliases' => $aliases,
+			':mailboxes' => $mailboxes,
+			':maxquota' => $maxquota,
+			':quota' => $quota,
+			':backupmx' => $backupmx,
+			':active' => $active,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':relay_all_recipients' => $relay_all_recipients
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['domain_added'], htmlspecialchars($domain))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_domain(array('domain' => $domain));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_add_alias($postarray) {
+  // Array elements
+  // address  string  (separated by " ", "," ";" "\n") - email address or domain
+  // goto     string  (separated by " ", "," ";" "\n")
+  // active   int
+	global $lang;
+	global $pdo;
+	$addresses  = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['address']));
+	$gotos      = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto']));
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if (empty($addresses[0])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_empty'])
+		);
+		return false;
+	}
+
+	if (empty($gotos[0])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['goto_empty'])
+		);
+		return false;
+	}
+
+	foreach ($addresses as $address) {
+		if (empty($address)) {
+			continue;
+		}
+
+		$domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
+		$local_part   = strstr($address, '@', true);
+		$address      = $local_part.'@'.$domain;
+
+		try {
+			$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+				WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
+			$stmt->execute(array(':domain1' => $domain, ':domain2' => $domain));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results == 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
+        );
+        return false;
+      }
+
+			$stmt = $pdo->prepare("SELECT `address` FROM `alias`
+				WHERE `address`= :address");
+			$stmt->execute(array(':address' => $address));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results != 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address))
+        );
+        return false;
+      }
+
+			$stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
+				WHERE `address`= :address");
+			$stmt->execute(array(':address' => $address));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results != 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($address))
+        );
+        return false;
+      }
+		}
+		catch(PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+
+		if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['alias_invalid'])
+			);
+			return false;
+		}
+
+		if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['access_denied'])
+			);
+			return false;
+		}
+
+		foreach ($gotos as &$goto) {
+			if (empty($goto)) {
+				continue;
+			}
+
+			$goto_domain		= idn_to_ascii(substr(strstr($goto, '@'), 1));
+			$goto_local_part	= strstr($goto, '@', true);
+			$goto				= $goto_local_part.'@'.$goto_domain;
+
+			if (!filter_var($goto, FILTER_VALIDATE_EMAIL) === true) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['goto_invalid'])
+				);
+				return false;
+			}
+			if ($goto == $address) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['alias_goto_identical'])
+				);
+				return false;
+			}
+		}
+
+		$gotos = array_filter($gotos);
+		$goto = implode(",", $gotos);
+
+		try {
+			$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`)
+				VALUES (:address, :goto, :domain, :created, :modified, :active)");
+
+			if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
+				$stmt->execute(array(
+					':address' => '@'.$domain,
+					':goto' => $goto,
+					':domain' => $domain,
+					':created' => date('Y-m-d H:i:s'),
+					':modified' => date('Y-m-d H:i:s'),
+					':active' => $active
+				));
+			}
+			else {
+				$stmt->execute(array(
+					':address' => $address,
+					':goto' => $goto,
+					':domain' => $domain,
+					':created' => date('Y-m-d H:i:s'),
+					':modified' => date('Y-m-d H:i:s'),
+					':active' => $active
+				));
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['alias_added'])
+			);
+		}
+		catch (PDOException $e) {
+      mailbox_delete_alias(array('address' => $address));
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_added'])
+	);
+}
+function mailbox_add_alias_domain($postarray) {
+  // Array elements
+  // active         int
+  // alias_domain   string
+  // target_domain  string
+	global $lang;
+	global $pdo;
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	$alias_domain     = idn_to_ascii(strtolower(trim($postarray['alias_domain'])));
+	$target_domain    = idn_to_ascii(strtolower(trim($postarray['target_domain'])));
+
+	if (!is_valid_domain_name($alias_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!is_valid_domain_name($target_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['target_domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $target_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	if ($alias_domain == $target_domain) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['aliasd_targetd_identical'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+			WHERE `domain`= :target_domain");
+		$stmt->execute(array(':target_domain' => $target_domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results == 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['targetd_not_found'])
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
+			UNION
+			SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain_in_domain");
+		$stmt->execute(array(':alias_domain' => $alias_domain, ':alias_domain_in_domain' => $alias_domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['aliasd_exists'])
+      );
+      return false;
+    }
+  }
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `created`, `modified`, `active`)
+			VALUES (:alias_domain, :target_domain, :created, :modified, :active)");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+			':target_domain' => $target_domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['aliasd_added'], htmlspecialchars($alias_domain))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_alias_domain(array('alias_domain' => $alias_domain));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_add_mailbox($postarray) {
+  // Array elements
+  // active             int
+  // local_part         string
+  // domain             string
+  // name               string    (username if empty)
+  // password           string
+  // password2          string
+  // quota              int       (MiB)
+  // active             int
+
+	global $pdo;
+	global $lang;
+	$local_part   = strtolower(trim($postarray['local_part']));
+	$domain       = idn_to_ascii(strtolower(trim($postarray['domain'])));
+  $username     = $local_part . '@' . $domain;
+	if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_invalid'])
+		);
+		return false;
+	}
+	if (empty($postarray['local_part'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_invalid'])
+		);
+		return false;
+	}
+	$password     = $postarray['password'];
+	$password2    = $postarray['password2'];
+	$name         = $postarray['name'];
+  $quota_m			= filter_var($postarray['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
+
+	if (empty($name)) {
+		$name = $local_part;
+	}
+
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+
+	$quota_b		= ($quota_m * 1048576);
+	$maildir		= $domain."/".$local_part."/";
+
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `mailboxes`, `maxquota`, `quota` FROM `domain`
+			WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT 
+			COUNT(*) as count,
+			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota`
+				FROM `mailbox`
+					WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT `local_part` FROM `mailbox` WHERE `local_part` = :local_part and `domain`= :domain");
+		$stmt->execute(array(':local_part' => $local_part, ':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE address= :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['is_alias'], htmlspecialchars($username))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($username))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results == 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
+      );
+      return false;
+    }
+  }
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	if (!is_numeric($quota_m) || $quota_m == "0") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['quota_not_0_not_numeric'])
+		);
+		return false;
+	}
+
+	if (!empty($password) && !empty($password2)) {
+		if ($password != $password2) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($password);
+	}
+	else {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['password_empty'])
+		);
+		return false;
+	}
+
+	if ($MailboxData['count'] >= $DomainData['mailboxes']) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['max_mailbox_exceeded'], $MailboxData['count'], $DomainData['mailboxes'])
+		);
+		return false;
+	}
+
+	if ($quota_m > $DomainData['maxquota']) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_exceeded'], $DomainData['maxquota'])
+		);
+		return false;
+	}
+
+	if (($MailboxData['quota'] + $quota_m) > $DomainData['quota']) {
+		$quota_left_m = ($DomainData['quota'] - $MailboxData['quota']);
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_left_exceeded'], $quota_left_m)
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`) 
+			VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :created, :modified, :active)");
+		$stmt->execute(array(
+			':username' => $username,
+			':password_hashed' => $password_hashed,
+			':name' => $name,
+			':maildir' => $maildir,
+			':quota_b' => $quota_b,
+			':local_part' => $local_part,
+			':domain' => $domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+
+		$stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
+			VALUES (:username, '0', '0')");
+		$stmt->execute(array(':username' => $username));
+
+		$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`)
+			VALUES (:username1, :username2, :domain, :created, :modified, :active)");
+		$stmt->execute(array(
+			':username1' => $username,
+			':username2' => $username,
+			':domain' => $domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['mailbox_added'], htmlspecialchars($username))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_mailbox(array('address' => $username));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_edit_alias_domain($postarray) {
+  // Array elements
+  // active             int
+  // alias_domain_now   string
+  // alias_domain       string
+	global $lang;
+	global $pdo;
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	$alias_domain       = idn_to_ascii(strtolower(trim($postarray['alias_domain'])));
+	$alias_domain_now   = strtolower(trim($postarray['alias_domain_now']));
+	if (!is_valid_domain_name($alias_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!is_valid_domain_name($alias_domain_now)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
+				WHERE `alias_domain`= :alias_domain_now");
+		$stmt->execute(array(':alias_domain_now' => $alias_domain_now));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
+		WHERE `target_domain`= :alias_domain");
+		$stmt->execute(array(':alias_domain' => $alias_domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if ($num_results != 0) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['aliasd_targetd_identical'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("UPDATE `alias_domain` SET
+      `alias_domain` = :alias_domain,
+      `active` = :active,
+      `modified` = :modified,
+        WHERE `alias_domain` = :alias_domain_now");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+      ':modified' => date('Y-m-d H:i:s'),
+			':alias_domain_now' => $alias_domain_now,
+			':active' => $active
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['aliasd_modified'], htmlspecialchars($alias_domain))
+	);
+}
+function mailbox_edit_alias($postarray) {
+  // Array elements
+  // address            string
+  // goto               string    (separated by " ", "," ";" "\n") - email address or domain
+  // active             int
+	global $lang;
+	global $pdo;
+	$address      = $postarray['address'];
+	$domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
+	$local_part   = strstr($address, '@', true);
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (empty($postarray['goto'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['goto_empty'])
+		);
+		return false;
+	}
+	$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto']));
+	foreach ($gotos as &$goto) {
+		if (empty($goto)) {
+			continue;
+		}
+		if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' =>sprintf($lang['danger']['goto_invalid'])
+			);
+			return false;
+		}
+		if ($goto == $address) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['alias_goto_identical'])
+			);
+			return false;
+		}
+	}
+	$gotos = array_filter($gotos);
+	$goto = implode(",", $gotos);
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_invalid'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("UPDATE `alias` SET
+      `goto` = :goto,
+      `active`= :active,
+      `modified` = :modified,
+        WHERE `address` = :address");
+		$stmt->execute(array(
+			':goto' => $goto,
+			':active' => $active,
+			':address' => $address,
+      ':modified' => date('Y-m-d H:i:s'),
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars($address))
+		);
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_edit_domain($postarray) {
+  // Array elements
+  // domain                 string
+  // description            string
+  // active                 int
+  // relay_all_recipients   int
+  // backupmx               int
+  // aliases                float
+  // mailboxes              float
+  // maxquota               float
+  // quota                  float     (Byte)
+  // active                 int
+
+	global $lang;
+	global $pdo;
+  
+  $domain       = idn_to_ascii($postarray['domain']);
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	if ($_SESSION['mailcow_cc_role'] == "domainadmin" && 	hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    $description  = $postarray['description'];
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+    try {
+      $stmt = $pdo->prepare("UPDATE `domain` SET 
+      `modified`= :modified,
+      `description` = :description
+        WHERE `domain` = :domain");
+      $stmt->execute(array(
+        ':modified' => date('Y-m-d H:i:s'),
+        ':description' => $description,
+        ':domain' => $domain
+      ));
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars($domain))
+      );
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  elseif ($_SESSION['mailcow_cc_role'] == "admin") {
+    $description  = $postarray['description'];
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+    $aliases		= filter_var($postarray['aliases'], FILTER_SANITIZE_NUMBER_FLOAT);
+    $mailboxes  = filter_var($postarray['mailboxes'], FILTER_SANITIZE_NUMBER_FLOAT);
+    $maxquota		= filter_var($postarray['maxquota'], FILTER_SANITIZE_NUMBER_FLOAT);
+    $quota			= filter_var($postarray['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
+    isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0';
+    isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0';
+    isset($postarray['relay_all_recipients']) ? $backupmx = '1' : true;
+    try {
+      // GET MAILBOX DATA
+      $stmt = $pdo->prepare("SELECT 
+          COUNT(*) AS count,
+          MAX(COALESCE(ROUND(`quota`/1048576), 0)) AS `maxquota`,
+          COALESCE(ROUND(SUM(`quota`)/1048576), 0) AS `quota`
+            FROM `mailbox`
+              WHERE domain= :domain");
+      $stmt->execute(array(':domain' => $domain));
+      $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
+      // GET ALIAS DATA
+      $stmt = $pdo->prepare("SELECT COUNT(*) AS `count` FROM `alias`
+          WHERE domain = :domain
+          AND address NOT IN (
+            SELECT `username` FROM `mailbox`
+          )");
+      $stmt->execute(array(':domain' => $domain));
+      $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
+    }
+    catch(PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+
+    if ($maxquota > $quota) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['mailbox_quota_exceeds_domain_quota'])
+      );
+      return false;
+    }
+
+    if ($MailboxData['maxquota'] > $maxquota) {
+      echo $MailboxData['maxquota'];
+      die();
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['max_quota_in_use'], $MailboxData['maxquota'])
+      );
+      return false;
+    }
+
+    if ($MailboxData['quota'] > $quota) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['domain_quota_m_in_use'], $MailboxData['quota'])
+      );
+      return false;
+    }
+
+    if ($MailboxData['count'] > $mailboxes) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['mailboxes_in_use'], $MailboxData['count'])
+      );
+      return false;
+    }
+
+    if ($AliasData['count'] > $aliases) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['aliases_in_use'], $AliasData['count'])
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("UPDATE `domain` SET 
+      `modified`= :modified,
+      `relay_all_recipients` = :relay_all_recipients,
+      `backupmx` = :backupmx,
+      `active` = :active,
+      `quota` = :quota,
+      `maxquota` = :maxquota,
+      `modified` = :modified,
+      `mailboxes` = :mailboxes,
+      `aliases` = :aliases,
+      `description` = :description
+        WHERE `domain` = :domain");
+      $stmt->execute(array(
+        ':relay_all_recipients' => $relay_all_recipients,
+        ':backupmx' => $backupmx,
+        ':active' => $active,
+        ':quota' => $quota,
+        ':maxquota' => $maxquota,
+        ':modified' => date('Y-m-d H:i:s'),
+        ':mailboxes' => $mailboxes,
+        ':aliases' => $aliases,
+        ':modified' => date('Y-m-d H:i:s'),
+        ':description' => $description,
+        ':domain' => $domain
+      ));
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars($domain))
+      );
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+}
+function mailbox_edit_mailbox($postarray) {
+	global $lang;
+	global $pdo;
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if (!filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	$quota_m      = $postarray['quota'];
+	$quota_b      = $quota_m*1048576;
+	$username     = $postarray['username'];
+	$name         = $postarray['name'];
+	$password     = $postarray['password'];
+	$password2    = $postarray['password2'];
+
+	try {
+		$stmt = $pdo->prepare("SELECT `domain`
+			FROM `mailbox`
+				WHERE username = :username");
+		$stmt->execute(array(':username' => $username));
+		$MailboxData1 = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT 
+			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota_m_now`
+				FROM `mailbox`
+					WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$MailboxData2 = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT 
+			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota_m_in_use`
+				FROM `mailbox`
+					WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $MailboxData1['domain']));
+		$MailboxData3 = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT `quota`, `maxquota`
+			FROM `domain`
+				WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $MailboxData1['domain']));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $MailboxData1['domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (!is_numeric($quota_m) || $quota_m == "0") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['quota_not_0_not_numeric'], htmlspecialchars($quota_m))
+		);
+		return false;
+	}
+	if ($quota_m > $DomainData['maxquota']) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_exceeded'], $DomainData['maxquota'])
+		);
+		return false;
+	}
+	if (($MailboxData3['quota_m_in_use'] - $MailboxData2['quota_m_now'] + $quota_m) > $DomainData['quota']) {
+		$quota_left_m = ($DomainData['quota'] - $MailboxData3['quota_m_in_use'] + $MailboxData2['quota_m_now']);
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_left_exceeded'], $quota_left_m)
+		);
+		return false;
+	}
+
+  // Get sender_acl items set by admin
+  $sender_acl_admin = array_merge(
+    mailbox_get_sender_acl_handles($username)['sender_acl_domains']['ro'],
+    mailbox_get_sender_acl_handles($username)['sender_acl_addresses']['ro']
+  );
+
+  // Get sender_acl items from POST array
+  (isset($postarray['sender_acl'])) ? $sender_acl_domain_admin = $postarray['sender_acl'] : $sender_acl_domain_admin = array();
+
+	if (!empty($sender_acl_domain_admin) || !empty($sender_acl_admin)) {
+    // Check items in POST array
+		foreach ($sender_acl_domain_admin as $sender_acl) {
+			if (!filter_var($sender_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($sender_acl, '@'))) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
+					);
+					return false;
+			}
+      if (is_valid_domain_name(ltrim($sender_acl, '@'))) {
+        if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($sender_acl, '@'))) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
+					);
+					return false;
+        }
+      }
+			if (filter_var($sender_acl, FILTER_VALIDATE_EMAIL)) {
+        if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $sender_acl)) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
+					);
+					return false;
+        }
+      }
+    }
+
+    // Merge both arrays
+    $sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
+
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+      $stmt->execute(array(
+        ':username' => $username
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+
+		foreach ($sender_acl_merged as $sender_acl) {
+      $domain = ltrim($sender_acl, '@');
+      if (is_valid_domain_name($domain)) {
+        $sender_acl = '@' . $domain;
+      }
+			try {
+				$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
+					VALUES (:sender_acl, :username)");
+				$stmt->execute(array(
+					':sender_acl' => $sender_acl,
+					':username' => $username
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+		}
+	}
+  else {
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+      $stmt->execute(array(
+        ':username' => $username
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+	if (!empty($password) && !empty($password2)) {
+		if ($password != $password2) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($password);
+		try {
+			$stmt = $pdo->prepare("UPDATE `alias` SET
+					`modified` = :modified,
+					`active` = :active
+						WHERE `address` = :address");
+			$stmt->execute(array(
+				':address' => $username,
+				':modified' => date('Y-m-d H:i:s'),
+				':active' => $active
+			));
+			$stmt = $pdo->prepare("UPDATE `mailbox` SET
+					`modified` = :modified,
+					`active` = :active,
+					`password` = :password_hashed,
+					`name`= :name,
+					`quota` = :quota_b
+						WHERE `username` = :username");
+			$stmt->execute(array(
+				':modified' => date('Y-m-d H:i:s'),
+				':password_hashed' => $password_hashed,
+				':active' => $active,
+				':name' => $name,
+				':quota_b' => $quota_b,
+				':username' => $username
+			));
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+			);
+			return true;
+		}
+		catch (PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	try {
+		$stmt = $pdo->prepare("UPDATE `alias` SET
+				`modified` = :modified,
+				`active` = :active
+					WHERE `address` = :address");
+		$stmt->execute(array(
+			':address' => $username,
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+		$stmt = $pdo->prepare("UPDATE `mailbox` SET
+				`modified` = :modified,
+				`active` = :active,
+				`name`= :name,
+				`quota` = :quota_b
+					WHERE `username` = :username");
+		$stmt->execute(array(
+			':active' => $active,
+			':modified' => date('Y-m-d H:i:s'),
+			':name' => $name,
+			':quota_b' => $quota_b,
+			':username' => $username
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+		);
+		return true;
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_get_mailboxes($domain = null) {
+	global $lang;
+	global $pdo;
+  $mailboxes = array();
+	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    try {
+      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `domain` != 'ALL' AND `domain` = :domain");
+      $stmt->execute(array(
+        ':domain' => $domain,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $mailboxes[] = $row['username'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  else {
+    try {
+      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
+      $stmt->execute(array(
+        ':username' => $_SESSION['mailcow_cc_username'],
+        ':role' => $_SESSION['mailcow_cc_role'],
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $mailboxes[] = $row['username'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  return $mailboxes;
+}
+function mailbox_get_alias_domains($domain = null) {
+  // Get all domains assigned to mailcow_cc_username or domain, if set
+  // Domain admin needs to be active
+  // Domain does not need to be active
+	global $lang;
+	global $pdo;
+  $aliasdomains = array();
+	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+  }
+  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    try {
+      $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain");
+      $stmt->execute(array(
+        ':domain' => $domain,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $aliasdomains[] = $row['alias_domain'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+	else {
+    try {
+      $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
+      $stmt->execute(array(
+        ':username' => $_SESSION['mailcow_cc_username'],
+        ':role' => $_SESSION['mailcow_cc_role'],
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $aliasdomains[] = $row['alias_domain'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  return $aliasdomains;
+}
+function mailbox_get_aliases($domain) {
+	global $lang;
+	global $pdo;
+  $aliases = array();
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+  try {
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
+    $stmt->execute(array(
+      ':domain' => $domain,
+    ));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $aliases[] = $row['address'];
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $aliases;
+}
+function mailbox_get_alias_details($address) {
+	global $lang;
+	global $pdo;
+  $aliasdata = array();
+  try {
+    $stmt = $pdo->prepare("SELECT
+      `domain`,
+      `goto`,
+      `address`,
+      `active` as `active_int`,
+      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+      `created`,
+      `modified`
+        FROM `alias`
+            WHERE `address` = :address AND `address` != `goto`");
+    $stmt->execute(array(
+      ':address' => $address,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $aliasdata['domain'] = $row['domain'];
+    $aliasdata['goto'] = $row['goto'];
+    $aliasdata['address'] = $row['address'];
+    (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
+    $aliasdata['active'] = $row['active'];
+    $aliasdata['active_int'] = $row['active_int'];
+    $aliasdata['created'] = $row['created'];
+    $aliasdata['modified'] = $row['modified'];
+    if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $aliasdata;
+}
+function mailbox_get_alias_domain_details($aliasdomain) {
+	global $lang;
+	global $pdo;
+  $aliasdomaindata = array();
+  try {
+    $stmt = $pdo->prepare("SELECT
+      `alias_domain`,
+      `target_domain`,
+      `active` AS `active_int`,
+      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+      `created`,
+      `modified`
+        FROM `alias_domain`
+            WHERE `alias_domain` = :aliasdomain");
+    $stmt->execute(array(
+      ':aliasdomain' => $aliasdomain,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $aliasdomaindata['alias_domain'] = $row['alias_domain'];
+    $aliasdomaindata['target_domain'] = $row['target_domain'];
+    $aliasdomaindata['active'] = $row['active'];
+    $aliasdomaindata['active_int'] = $row['active_int'];
+    $aliasdomaindata['created'] = $row['created'];
+    $aliasdomaindata['modified'] = $row['modified'];
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdomaindata['target_domain'])) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  return $aliasdomaindata;
+}
+function mailbox_get_domains() {
+  // Get all domains assigned to mailcow_cc_username
+  // Domain admin needs to be active
+  // Domain does not need to be active
+	global $lang;
+	global $pdo;
+
+  try {
+    $domains = array();
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE (`domain` IN (
+        SELECT `domain` from `domain_admins`
+          WHERE (`active`='1' AND `username` = :username))
+        )
+        OR ('admin'= :role)
+        AND `domain` != 'ALL'");
+    $stmt->execute(array(
+      ':username' => $_SESSION['mailcow_cc_username'],
+      ':role' => $_SESSION['mailcow_cc_role'],
+    ));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $domains[] = $row['domain'];
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $domains;
+}
+function mailbox_get_domain_details($domain) {
+	global $lang;
+	global $pdo;
+
+  $domaindata = array();
+	$domain = idn_to_ascii(strtolower(trim($domain)));
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+  try {
+    $stmt = $pdo->prepare("SELECT 
+        `domain`,
+        `description`,
+        `aliases`,
+        `mailboxes`, 
+        `maxquota`,
+        `quota`,
+        `relay_all_recipients` as `relay_all_recipients_int`,
+        `backupmx` as `backupmx_int`,
+        `active` as `active_int`,
+        CASE `relay_all_recipients` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `relay_all_recipients`,
+        CASE `backupmx` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `backupmx`,
+        CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+          FROM `domain` WHERE `domain`= :domain");
+    $stmt->execute(array(
+      ':domain' => $domain,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `domain` = :domain");
+    $stmt->execute(array(':domain' => $row['domain']));
+    $MailboxDataDomain	= $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $domaindata['max_new_mailbox_quota']	= ($row['quota'] * 1048576) - $MailboxDataDomain['in_use'];
+    if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
+      $domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
+    }
+    $domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use'];
+    $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
+    $domaindata['mboxes_left'] = $row['mailboxes']	- $MailboxDataDomain['count'];
+    $domaindata['domain_name'] = $row['domain'];
+    $domaindata['description'] = $row['description'];
+    $domaindata['max_num_aliases_for_domain'] = $row['aliases'];
+    $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
+    $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576;
+    $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576;
+    $domaindata['backupmx'] = $row['backupmx'];
+    $domaindata['backupmx_int'] = $row['backupmx_int'];
+    $domaindata['active'] = $row['active'];
+    $domaindata['active_int'] = $row['active_int'];
+    $domaindata['relay_all_recipients'] = $row['relay_all_recipients'];
+    $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients_int'];
+
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias`
+      WHERE `domain`= :domain
+        AND `address` NOT IN (
+          SELECT `username` FROM `mailbox`
+        )");
+    $stmt->execute(array(
+      ':domain' => $domain,
+    ));
+    $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
+    (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0";
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+
+  return $domaindata;
+}
+function mailbox_get_mailbox_details($mailbox) {
+	global $lang;
+	global $pdo;
+  $mailboxdata = array();
+  try {
+    $stmt = $pdo->prepare("SELECT
+        `domain`.`backupmx`,
+        `mailbox`.`username`,
+        `mailbox`.`name`,
+        `mailbox`.`active` AS `active_int`,
+        CASE `mailbox`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+        `mailbox`.`domain`,
+        `mailbox`.`quota`,
+        `quota2`.`bytes`,
+        `quota2`.`messages`
+          FROM `mailbox`, `quota2`, `domain`
+            WHERE `mailbox`.`username` = `quota2`.`username` AND `domain`.`domain` = `mailbox`.`domain` AND `mailbox`.`username` = :mailbox");
+    $stmt->execute(array(
+      ':mailbox' => $mailbox,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM  `domain` WHERE `domain` = :domain");
+    $stmt->execute(array(':domain' => $row['domain']));
+    $DomainQuota  = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `domain` = :domain AND `username` != :username");
+    $stmt->execute(array(':domain' => $row['domain'], ':username' => $row['username']));
+    $MailboxUsage	= $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
+    if ($mailboxdata['max_new_quota'] > ($DomainQuota['maxquota'] * 1048576)) {
+      $mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
+    }
+
+    $mailboxdata['username'] = $row['username'];
+    $mailboxdata['is_relayed'] = $row['backupmx'];
+    $mailboxdata['name'] = $row['name'];
+    $mailboxdata['active'] = $row['active'];
+    $mailboxdata['active_int'] = $row['active_int'];
+    $mailboxdata['domain'] = $row['domain'];
+    $mailboxdata['quota'] = $row['quota'];
+    $mailboxdata['quota_used'] = intval($row['bytes']);
+    $mailboxdata['percent_in_use'] = round((intval($row['bytes']) / intval($row['quota'])) * 100);
+    $mailboxdata['messages'] = $row['messages'];
+    if ($mailboxdata['percent_in_use'] >= 90) {
+      $mailboxdata['percent_class'] = "danger";
+    }
+    elseif ($mailboxdata['percent_in_use'] >= 75) {
+      $mailboxdata['percent_class'] = "warning";
+    }
+    else {
+      $mailboxdata['percent_class'] = "success";
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  if (!isset($mailboxdata['domain']) ||
+    (isset($mailboxdata['domain']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $mailboxdata['domain']))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  
+  return $mailboxdata;
+}
+function mailbox_delete_domain($postarray) {
+	global $lang;
+	global $pdo;
+	$domain = $postarray['domain'];
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+	$domain	= strtolower(trim($domain));
+
+
+	try {
+		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
+			WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if ($num_results != 0 || !empty($num_results)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_not_empty'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `domain` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `target_domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` LIKE :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `address` = :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['domain_removed'], htmlspecialchars($domain))
+	);
+	return true;
+}
+function mailbox_delete_alias($postarray) {
+	global $lang;
+	global $pdo;
+	$address		= $postarray['address'];
+	$local_part		= strstr($address, '@', true);
+  $domain = mailbox_get_alias_details($address)['domain'];
+	try {
+		$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address");
+		$stmt->execute(array(':address' => $address));
+		$gotos = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$goto_array = explode(',', $gotos['goto']);
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)");
+		$stmt->execute(array(
+			':address' => $address
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars($address))
+	);
+
+}
+function mailbox_delete_alias_domain($postarray) {
+	global $lang;
+	global $pdo;
+	if (!is_valid_domain_name($postarray['alias_domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+	$alias_domain = $postarray['alias_domain'];
+	try {
+		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
+			WHERE `alias_domain`= :alias_domain");
+		$stmt->execute(array(':alias_domain' => $alias_domain));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :alias_domain");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_domain_removed'], htmlspecialchars($alias_domain))
+	);
+}
+function mailbox_delete_mailbox($postarray) {
+	global $lang;
+	global $pdo;
+	$username	= $postarray['username'];
+  $domain = mailbox_get_mailbox_details($username)['domain'];
+	if (!filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `user2` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias`
+				WHERE `goto` LIKE :username");
+		$stmt->execute(array(':username' => '%'.$username.'%'));
+		$GotoData = $stmt->fetchAll(PDO::FETCH_ASSOC);
+		foreach ($GotoData as $gotos) {
+			$goto_exploded = explode(',', $gotos['goto']);
+			if (($key = array_search($username, $goto_exploded)) !== false) {
+				unset($goto_exploded[$key]);
+			}
+			$gotos_rebuild = implode(',', $goto_exploded);
+			$stmt = $pdo->prepare("UPDATE `alias` SET
+        `goto` = :goto,
+        `modified` = :modified,
+          WHERE `address` = :address");
+			$stmt->execute(array(
+				':goto' => $gotos_rebuild,
+        ':modified' => date('Y-m-d H:i:s'),
+				':address' => $gotos['address']
+			));
+		}
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['mailbox_removed'], htmlspecialchars($username))
+	);
+}
+function mailbox_get_sender_acl_handles($mailbox) {
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+	}
+
+  $data['sender_acl_domains']['ro']               = array();
+  $data['sender_acl_domains']['rw']               = array();
+  $data['sender_acl_domains']['selectable']       = array();
+  $data['sender_acl_addresses']['ro']             = array();
+  $data['sender_acl_addresses']['rw']             = array();
+  $data['sender_acl_addresses']['selectable']     = array();
+  $data['fixed_sender_aliases']                   = array();
+  
+  try {
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` = :goto AND `address` NOT LIKE '@%'");
+    $stmt->execute(array(':goto' => $mailbox));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($rows)) {
+      $data['fixed_sender_aliases'][] = $row['address'];
+    }
+
+    // Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
+    // Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
+    $stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `send_as` LIKE '@%'");
+    $stmt->execute(array(':logged_in_as' => $mailbox));
+    $domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($domain_row = array_shift($domain_rows)) {
+      if (is_valid_domain_name($domain_row['send_as']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
+        $data['sender_acl_domains']['ro'][] = $domain_row['send_as'];
+        continue;
+      }
+      if (is_valid_domain_name($domain_row['send_as']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
+        $data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
+        continue;
+      }
+    }
+
+    $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `send_as` NOT LIKE '@%'");
+    $stmt->execute(array(':logged_in_as' => $mailbox));
+    $address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($address_row = array_shift($address_rows)) {
+      if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
+        $data['sender_acl_addresses']['ro'][] = $address_row['send_as'];
+        continue;
+      }
+      if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
+        $data['sender_acl_addresses']['rw'][] = $address_row['send_as'];
+        continue;
+      }
+    }
+
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` NOT IN (
+        SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` 
+          WHERE `logged_in_as` = :logged_in_as
+            AND `send_as` LIKE '@%')");
+    $stmt->execute(array(
+      ':logged_in_as' => $mailbox,
+    ));
+    $rows_domain = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row_domain = array_shift($rows_domain)) {
+      if (is_valid_domain_name($row_domain['domain']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row_domain['domain'])) {
+        $data['sender_acl_domains']['selectable'][] = $row_domain['domain'];
+        $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
+          WHERE `target_domain` = :target_domain
+            AND `alias_domain` NOT IN (
+            SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` 
+              WHERE `logged_in_as` = :logged_in_as
+                AND `send_as` LIKE '@%')");
+        $stmt->execute(array(
+          ':target_domain' => $row_domain['domain'],
+          ':logged_in_as' => $mailbox,
+        ));
+        $rows_ad = $stmt->fetchAll(PDO::FETCH_ASSOC);
+        while ($row_ad = array_shift($rows_ad)) {
+          if (is_valid_domain_name($row_ad['alias_domain']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row_ad['alias_domain'])) {
+            $data['sender_acl_domains']['selectable'][] = $row_ad['alias_domain'];
+          }
+        }
+      }
+    }
+
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias`
+      WHERE `goto` != :goto
+        AND `address` NOT IN (
+          SELECT `send_as` FROM `sender_acl` 
+            WHERE `logged_in_as` = :logged_in_as
+              AND `send_as` NOT LIKE '@%')");
+    $stmt->execute(array(
+      ':logged_in_as' => $mailbox,
+      ':goto' => $mailbox
+    ));
+    while ($row = array_shift($rows)) {
+      if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
+        $data['sender_acl_addresses']['selectable'][] = $row['address'];
+      }
+    }
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $data;
+}
+function get_u2f_registrations($username) {
+  global $pdo;
+  $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `username` = ?");
+  $sel->execute(array($username));
+  return $sel->fetchAll(PDO::FETCH_OBJ);
+}
+function add_u2f_registration($username, $reg) {
+  global $pdo;
+  global $lang;
+  $ins = $pdo->prepare("INSERT INTO `tfa` (`username`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`) values (?, 'u2f', ?, ?, ?, ?)");
+  $ins->execute(array($username, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['object_modified'], $username)
+	);
+}
+function edit_u2f_registration($reg) {
+  global $pdo;
+  $upd = $pdo->prepare("update tfa set counter = ? where id = ?");
+  $upd->execute(array($reg->counter, $reg->id));
+}
 ?>
 ?>

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

@@ -68,11 +68,11 @@
 								<li <?=(preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/mailbox.php"><?=$lang['header']['mailboxes'];?></a></li>
 								<li <?=(preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/mailbox.php"><?=$lang['header']['mailboxes'];?></a></li>
 							<?php
 							<?php
 							}
 							}
-							if ($_SESSION['mailcow_cc_role'] == "user") {
+							if ($_SESSION['mailcow_cc_role'] != "admin") {
 							?>
 							?>
 								<li <?=(preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/user.php"><?=$lang['header']['user_settings'];?></a></li>
 								<li <?=(preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/user.php"><?=$lang['header']['user_settings'];?></a></li>
 							<?php
 							<?php
-							}
+              }
 						}
 						}
 						?>
 						?>
 					</ul>
 					</ul>

+ 14 - 1
data/web/inc/init.sql

@@ -127,6 +127,19 @@ CREATE TABLE IF NOT EXISTS `imapsync` (
   PRIMARY KEY (`id`)
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
 
 
+CREATE TABLE IF NOT EXISTS `tfa` (
+  `id` INT NOT NULL AUTO_INCREMENT,
+  `username` VARCHAR(255) NOT NULL,
+  `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp'),
+  `secret` VARCHAR(255) DEFAULT NULL,
+  `keyHandle` VARCHAR(255) DEFAULT NULL,
+  `publicKey` VARCHAR(255) DEFAULT NULL,
+  `counter` INT NOT NULL DEFAULT '0',
+  `certificate` TEXT,
+  `active` TINYINT(1) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
 DROP VIEW IF EXISTS grouped_mail_aliases;
 DROP VIEW IF EXISTS grouped_mail_aliases;
 DROP VIEW IF EXISTS grouped_sender_acl;
 DROP VIEW IF EXISTS grouped_sender_acl;
 DROP VIEW IF EXISTS grouped_domain_alias_address;
 DROP VIEW IF EXISTS grouped_domain_alias_address;
@@ -148,7 +161,7 @@ SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '
 LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;
 LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;
 
 
 CREATE TABLE IF NOT EXISTS sogo_acl (
 CREATE TABLE IF NOT EXISTS sogo_acl (
-	c_folder_id INTeger NOT NULL,
+	c_folder_id INTEGER NOT NULL,
 	c_object character varying(255) NOT NULL,
 	c_object character varying(255) NOT NULL,
 	c_uid character varying(255) NOT NULL,
 	c_uid character varying(255) NOT NULL,
 	c_role character varying(80) NOT NULL
 	c_role character varying(80) NOT NULL

+ 506 - 0
data/web/inc/lib/U2F.php

@@ -0,0 +1,506 @@
+<?php
+/* Copyright (c) 2014 Yubico AB
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above
+ *     copyright notice, this list of conditions and the following
+ *     disclaimer in the documentation and/or other materials provided
+ *     with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+namespace u2flib_server;
+
+/** Constant for the version of the u2f protocol */
+const U2F_VERSION = "U2F_V2";
+
+/** Error for the authentication message not matching any outstanding
+ * authentication request */
+const ERR_NO_MATCHING_REQUEST = 1;
+
+/** Error for the authentication message not matching any registration */
+const ERR_NO_MATCHING_REGISTRATION = 2;
+
+/** Error for the signature on the authentication message not verifying with
+ * the correct key */
+const ERR_AUTHENTICATION_FAILURE = 3;
+
+/** Error for the challenge in the registration message not matching the
+ * registration challenge */
+const ERR_UNMATCHED_CHALLENGE = 4;
+
+/** Error for the attestation signature on the registration message not
+ * verifying */
+const ERR_ATTESTATION_SIGNATURE = 5;
+
+/** Error for the attestation verification not verifying */
+const ERR_ATTESTATION_VERIFICATION = 6;
+
+/** Error for not getting good random from the system */
+const ERR_BAD_RANDOM = 7;
+
+/** Error when the counter is lower than expected */
+const ERR_COUNTER_TOO_LOW = 8;
+
+/** Error decoding public key */
+const ERR_PUBKEY_DECODE = 9;
+
+/** Error user-agent returned error */
+const ERR_BAD_UA_RETURNING = 10;
+
+/** Error old OpenSSL version */
+const ERR_OLD_OPENSSL = 11;
+
+/** @internal */
+const PUBKEY_LEN = 65;
+
+class U2F
+{
+    /** @var string  */
+    private $appId;
+
+    /** @var null|string */
+    private $attestDir;
+
+    /** @internal */
+    private $FIXCERTS = array(
+        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
+        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
+        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
+        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
+        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
+        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
+    );
+
+    /**
+     * @param string $appId Application id for the running application
+     * @param string|null $attestDir Directory where trusted attestation roots may be found
+     * @throws Error If OpenSSL older than 1.0.0 is used
+     */
+    public function __construct($appId, $attestDir = null)
+    {
+        if(OPENSSL_VERSION_NUMBER < 0x10000000) {
+            throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
+        }
+        $this->appId = $appId;
+        $this->attestDir = $attestDir;
+    }
+
+    /**
+     * Called to get a registration request to send to a user.
+     * Returns an array of one registration request and a array of sign requests.
+     *
+     * @param array $registrations List of current registrations for this
+     * user, to prevent the user from registering the same authenticator several
+     * times.
+     * @return array An array of two elements, the first containing a
+     * RegisterRequest the second being an array of SignRequest
+     * @throws Error
+     */
+    public function getRegisterData(array $registrations = array())
+    {
+        $challenge = $this->createChallenge();
+        $request = new RegisterRequest($challenge, $this->appId);
+        $signs = $this->getAuthenticateData($registrations);
+        return array($request, $signs);
+    }
+
+    /**
+     * Called to verify and unpack a registration message.
+     *
+     * @param RegisterRequest $request this is a reply to
+     * @param object $response response from a user
+     * @param bool $includeCert set to true if the attestation certificate should be
+     * included in the returned Registration object
+     * @return Registration
+     * @throws Error
+     */
+    public function doRegister($request, $response, $includeCert = true)
+    {
+        if( !is_object( $request ) ) {
+            throw new \InvalidArgumentException('$request of doRegister() method only accepts object.');
+        }
+
+        if( !is_object( $response ) ) {
+            throw new \InvalidArgumentException('$response of doRegister() method only accepts object.');
+        }
+
+        if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
+            throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
+        }
+
+        if( !is_bool( $includeCert ) ) {
+            throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
+        }
+
+        $rawReg = $this->base64u_decode($response->registrationData);
+        $regData = array_values(unpack('C*', $rawReg));
+        $clientData = $this->base64u_decode($response->clientData);
+        $cli = json_decode($clientData);
+
+        if($cli->challenge !== $request->challenge) {
+            throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE );
+        }
+
+        $registration = new Registration();
+        $offs = 1;
+        $pubKey = substr($rawReg, $offs, PUBKEY_LEN);
+        $offs += PUBKEY_LEN;
+        // decode the pubKey to make sure it's good
+        $tmpKey = $this->pubkey_to_pem($pubKey);
+        if($tmpKey === null) {
+            throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
+        }
+        $registration->publicKey = base64_encode($pubKey);
+        $khLen = $regData[$offs++];
+        $kh = substr($rawReg, $offs, $khLen);
+        $offs += $khLen;
+        $registration->keyHandle = $this->base64u_encode($kh);
+
+        // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
+        $certLen = 4;
+        $certLen += ($regData[$offs + 2] << 8);
+        $certLen += $regData[$offs + 3];
+
+        $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
+        $offs += $certLen;
+        $pemCert  = "-----BEGIN CERTIFICATE-----\r\n";
+        $pemCert .= chunk_split(base64_encode($rawCert), 64);
+        $pemCert .= "-----END CERTIFICATE-----";
+        if($includeCert) {
+            $registration->certificate = base64_encode($rawCert);
+        }
+        if($this->attestDir) {
+            if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
+                throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION );
+            }
+        }
+
+        if(!openssl_pkey_get_public($pemCert)) {
+            throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
+        }
+        $signature = substr($rawReg, $offs);
+
+        $dataToVerify  = chr(0);
+        $dataToVerify .= hash('sha256', $request->appId, true);
+        $dataToVerify .= hash('sha256', $clientData, true);
+        $dataToVerify .= $kh;
+        $dataToVerify .= $pubKey;
+
+        if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
+            return $registration;
+        } else {
+            throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE );
+        }
+    }
+
+    /**
+     * Called to get an authentication request.
+     *
+     * @param array $registrations An array of the registrations to create authentication requests for.
+     * @return array An array of SignRequest
+     * @throws Error
+     */
+    public function getAuthenticateData(array $registrations)
+    {
+        $sigs = array();
+        foreach ($registrations as $reg) {
+            if( !is_object( $reg ) ) {
+                throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
+            }
+
+            $sig = new SignRequest();
+            $sig->appId = $this->appId;
+            $sig->keyHandle = $reg->keyHandle;
+            $sig->challenge = $this->createChallenge();
+            $sigs[] = $sig;
+        }
+        return $sigs;
+    }
+
+    /**
+     * Called to verify an authentication response
+     *
+     * @param array $requests An array of outstanding authentication requests
+     * @param array $registrations An array of current registrations
+     * @param object $response A response from the authenticator
+     * @return Registration
+     * @throws Error
+     *
+     * The Registration object returned on success contains an updated counter
+     * that should be saved for future authentications.
+     * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
+     * token cloning or similar and appropriate action should be taken.
+     */
+    public function doAuthenticate(array $requests, array $registrations, $response)
+    {
+        if( !is_object( $response ) ) {
+            throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.');
+        }
+
+        if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
+            throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
+        }
+
+        /** @var object|null $req */
+        $req = null;
+
+        /** @var object|null $reg */
+        $reg = null;
+
+        $clientData = $this->base64u_decode($response->clientData);
+        $decodedClient = json_decode($clientData);
+        foreach ($requests as $req) {
+            if( !is_object( $req ) ) {
+                throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.');
+            }
+
+            if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
+                break;
+            }
+
+            $req = null;
+        }
+        if($req === null) {
+            throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST );
+        }
+        foreach ($registrations as $reg) {
+            if( !is_object( $reg ) ) {
+                throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.');
+            }
+
+            if($reg->keyHandle === $response->keyHandle) {
+                break;
+            }
+            $reg = null;
+        }
+        if($reg === null) {
+            throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION );
+        }
+        $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey));
+        if($pemKey === null) {
+            throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
+        }
+
+        $signData = $this->base64u_decode($response->signatureData);
+        $dataToVerify  = hash('sha256', $req->appId, true);
+        $dataToVerify .= substr($signData, 0, 5);
+        $dataToVerify .= hash('sha256', $clientData, true);
+        $signature = substr($signData, 5);
+
+        if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
+            $ctr = unpack("Nctr", substr($signData, 1, 4));
+            $counter = $ctr['ctr'];
+            /* TODO: wrap-around should be handled somehow.. */
+            if($counter > $reg->counter) {
+                $reg->counter = $counter;
+                return $reg;
+            } else {
+                throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW );
+            }
+        } else {
+            throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE );
+        }
+    }
+
+    /**
+     * @return array
+     */
+    private function get_certs()
+    {
+        $files = array();
+        $dir = $this->attestDir;
+        if($dir && $handle = opendir($dir)) {
+            while(false !== ($entry = readdir($handle))) {
+                if(is_file("$dir/$entry")) {
+                    $files[] = "$dir/$entry";
+                }
+            }
+            closedir($handle);
+        }
+        return $files;
+    }
+
+    /**
+     * @param string $data
+     * @return string
+     */
+    private function base64u_encode($data)
+    {
+        return trim(strtr(base64_encode($data), '+/', '-_'), '=');
+    }
+
+    /**
+     * @param string $data
+     * @return string
+     */
+    private function base64u_decode($data)
+    {
+        return base64_decode(strtr($data, '-_', '+/'));
+    }
+
+    /**
+     * @param string $key
+     * @return null|string
+     */
+    private function pubkey_to_pem($key)
+    {
+        if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") {
+            return null;
+        }
+
+        /*
+         * Convert the public key to binary DER format first
+         * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
+         *
+         *  SEQUENCE(2 elem)                        30 59
+         *   SEQUENCE(2 elem)                       30 13
+         *    OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
+         *    OID1.2.840.10045.3.1.7 (secp256r1)    06 08 2a 86 48 ce 3d 03 01 07
+         *   BIT STRING(520 bit)                    03 42 ..key..
+         */
+        $der  = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
+        $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
+        $der .= "\0".$key;
+
+        $pem  = "-----BEGIN PUBLIC KEY-----\r\n";
+        $pem .= chunk_split(base64_encode($der), 64);
+        $pem .= "-----END PUBLIC KEY-----";
+
+        return $pem;
+    }
+
+    /**
+     * @return string
+     * @throws Error
+     */
+    private function createChallenge()
+    {
+        $challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
+        if( $crypto_strong !== true ) {
+            throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM);
+        }
+
+        $challenge = $this->base64u_encode( $challenge );
+
+        return $challenge;
+    }
+
+    /**
+     * Fixes a certificate where the signature contains unused bits.
+     *
+     * @param string $cert
+     * @return mixed
+     */
+    private function fixSignatureUnusedBits($cert)
+    {
+        if(in_array(hash('sha256', $cert), $this->FIXCERTS)) {
+            $cert[strlen($cert) - 257] = "\0";
+        }
+        return $cert;
+    }
+}
+
+/**
+ * Class for building a registration request
+ *
+ * @package u2flib_server
+ */
+class RegisterRequest
+{
+    /** Protocol version */
+    public $version = U2F_VERSION;
+
+    /** Registration challenge */
+    public $challenge;
+
+    /** Application id */
+    public $appId;
+
+    /**
+     * @param string $challenge
+     * @param string $appId
+     * @internal
+     */
+    public function __construct($challenge, $appId)
+    {
+        $this->challenge = $challenge;
+        $this->appId = $appId;
+    }
+}
+
+/**
+ * Class for building up an authentication request
+ *
+ * @package u2flib_server
+ */
+class SignRequest
+{
+    /** Protocol version */
+    public $version = U2F_VERSION;
+
+    /** Authentication challenge */
+    public $challenge;
+
+    /** Key handle of a registered authenticator */
+    public $keyHandle;
+
+    /** Application id */
+    public $appId;
+}
+
+/**
+ * Class returned for successful registrations
+ *
+ * @package u2flib_server
+ */
+class Registration
+{
+    /** The key handle of the registered authenticator */
+    public $keyHandle;
+
+    /** The public key of the registered authenticator */
+    public $publicKey;
+
+    /** The attestation certificate of the registered authenticator */
+    public $certificate;
+
+    /** The counter associated with this registration */
+    public $counter = -1;
+}
+
+/**
+ * Error class, returned on errors
+ *
+ * @package u2flib_server
+ */
+class Error extends \Exception
+{
+    /**
+     * Override constructor and make message and code mandatory
+     * @param string $message
+     * @param int $code
+     * @param \Exception|null $previous
+     */
+    public function __construct($message, $code, \Exception $previous = null) {
+        parent::__construct($message, $code, $previous);
+    }
+}

+ 475 - 0
data/web/inc/lib/Yubico.php

@@ -0,0 +1,475 @@
+<?php
+  /**
+   * Class for verifying Yubico One-Time-Passcodes
+   *
+   * @category    Auth
+   * @package     Auth_Yubico
+   * @author      Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>
+   * @copyright   2007-2015 Yubico AB
+   * @license     http://opensource.org/licenses/bsd-license.php New BSD License
+   * @version     2.0
+   * @link        http://www.yubico.com/
+   */
+
+require_once 'PEAR.php';
+
+/**
+ * Class for verifying Yubico One-Time-Passcodes
+ *
+ * Simple example:
+ * <code>
+ * require_once 'Auth/Yubico.php';
+ * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
+ *
+ * # Generate a new id+key from https://api.yubico.com/get-api-key/
+ * $yubi = new Auth_Yubico('42', 'FOOBAR=');
+ * $auth = $yubi->verify($otp);
+ * if (PEAR::isError($auth)) {
+ *    print "<p>Authentication failed: " . $auth->getMessage();
+ *    print "<p>Debug output from server: " . $yubi->getLastResponse();
+ * } else {
+ *    print "<p>You are authenticated!";
+ * }
+ * </code>
+ */
+class Auth_Yubico
+{
+	/**#@+
+	 * @access private
+	 */
+
+	/**
+	 * Yubico client ID
+	 * @var string
+	 */
+	var $_id;
+
+	/**
+	 * Yubico client key
+	 * @var string
+	 */
+	var $_key;
+
+	/**
+	 * URL part of validation server
+	 * @var string
+	 */
+	var $_url;
+
+	/**
+	 * List with URL part of validation servers
+	 * @var array
+	 */
+	var $_url_list;
+
+	/**
+	 * index to _url_list
+	 * @var int
+	 */
+	var $_url_index;
+
+	/**
+	 * Last query to server
+	 * @var string
+	 */
+	var $_lastquery;
+
+	/**
+	 * Response from server
+	 * @var string
+	 */
+	var $_response;
+
+	/**
+	 * Flag whether to use https or not.
+	 * @var boolean
+	 */
+	var $_https;
+
+	/**
+	 * Flag whether to verify HTTPS server certificates or not.
+	 * @var boolean
+	 */
+	var $_httpsverify;
+
+	/**
+	 * Constructor
+	 *
+	 * Sets up the object
+	 * @param    string  $id     The client identity
+	 * @param    string  $key    The client MAC key (optional)
+	 * @param    boolean $https  Flag whether to use https (optional)
+	 * @param    boolean $httpsverify  Flag whether to use verify HTTPS
+	 *                                 server certificates (optional,
+	 *                                 default true)
+	 * @access public
+	 */
+	function __construct($id, $key = '', $https = 0, $httpsverify = 1)
+	{
+		$this->_id =  $id;
+		$this->_key = base64_decode($key);
+		$this->_https = $https;
+		$this->_httpsverify = $httpsverify;
+	}
+  
+  function Auth_Yubico($id, $key = '', $https = 0, $httpsverify = 1)
+	{
+    self::__construct();
+	}
+
+	/**
+	 * Specify to use a different URL part for verification.
+	 * The default is "api.yubico.com/wsapi/verify".
+	 *
+	 * @param  string $url  New server URL part to use
+	 * @access public
+	 */
+	function setURLpart($url)
+	{
+		$this->_url = $url;
+	}
+
+	/**
+	 * Get URL part to use for validation.
+	 *
+	 * @return string  Server URL part
+	 * @access public
+	 */
+	function getURLpart()
+	{
+		if ($this->_url) {
+			return $this->_url;
+		} else {
+			return "api.yubico.com/wsapi/verify";
+		}
+	}
+
+
+	/**
+	 * Get next URL part from list to use for validation.
+	 *
+	 * @return mixed string with URL part of false if no more URLs in list
+	 * @access public
+	 */
+	function getNextURLpart()
+	{
+	  if ($this->_url_list) $url_list=$this->_url_list;
+	  else $url_list=array('api.yubico.com/wsapi/2.0/verify',
+			       'api2.yubico.com/wsapi/2.0/verify', 
+			       'api3.yubico.com/wsapi/2.0/verify', 
+			       'api4.yubico.com/wsapi/2.0/verify',
+			       'api5.yubico.com/wsapi/2.0/verify');
+	  
+	  if ($this->_url_index>=count($url_list)) return false;
+	  else return $url_list[$this->_url_index++];
+	}
+
+	/**
+	 * Resets index to URL list
+	 *
+	 * @access public
+	 */
+	function URLreset()
+	{
+	  $this->_url_index=0;
+	}
+
+	/**
+	 * Add another URLpart.
+	 *
+	 * @access public
+	 */
+	function addURLpart($URLpart) 
+	{
+	  $this->_url_list[]=$URLpart;
+	}
+	
+	/**
+	 * Return the last query sent to the server, if any.
+	 *
+	 * @return string  Request to server
+	 * @access public
+	 */
+	function getLastQuery()
+	{
+		return $this->_lastquery;
+	}
+
+	/**
+	 * Return the last data received from the server, if any.
+	 *
+	 * @return string  Output from server
+	 * @access public
+	 */
+	function getLastResponse()
+	{
+		return $this->_response;
+	}
+
+	/**
+	 * Parse input string into password, yubikey prefix,
+	 * ciphertext, and OTP.
+	 *
+	 * @param  string    Input string to parse
+	 * @param  string    Optional delimiter re-class, default is '[:]'
+	 * @return array     Keyed array with fields
+	 * @access public
+	 */
+	function parsePasswordOTP($str, $delim = '[:]')
+	{
+	  if (!preg_match("/^((.*)" . $delim . ")?" .
+			  "(([cbdefghijklnrtuv]{0,16})" .
+			  "([cbdefghijklnrtuv]{32}))$/i",
+			  $str, $matches)) {
+	    /* Dvorak? */
+	    if (!preg_match("/^((.*)" . $delim . ")?" .
+			    "(([jxe\.uidchtnbpygk]{0,16})" .
+			    "([jxe\.uidchtnbpygk]{32}))$/i",
+			    $str, $matches)) {
+	      return false;
+	    } else {
+	      $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
+	    }
+	  } else {
+	    $ret['otp'] = $matches[3];
+	  }
+	  $ret['password'] = $matches[2];
+	  $ret['prefix'] = $matches[4];
+	  $ret['ciphertext'] = $matches[5];
+	  return $ret;
+	}
+
+	/* TODO? Add functions to get parsed parts of server response? */
+
+	/**
+	 * Parse parameters from last response
+	 *
+	 * example: getParameters("timestamp", "sessioncounter", "sessionuse");
+	 *
+	 * @param  array @parameters  Array with strings representing
+	 *                            parameters to parse
+	 * @return array  parameter array from last response
+	 * @access public
+	 */
+	function getParameters($parameters)
+	{
+	  if ($parameters == null) {
+	    $parameters = array('timestamp', 'sessioncounter', 'sessionuse');
+	  }
+	  $param_array = array();
+	  foreach ($parameters as $param) {
+	    if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
+	      return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
+	    }
+	    $param_array[$param]=$out[1];
+	  }
+	  return $param_array;
+	}
+
+	/**
+	 * Verify Yubico OTP against multiple URLs
+	 * Protocol specification 2.0 is used to construct validation requests
+	 *
+	 * @param string $token        Yubico OTP
+	 * @param int $use_timestamp   1=>send request with &timestamp=1 to
+	 *                             get timestamp and session information
+	 *                             in the response
+	 * @param boolean $wait_for_all  If true, wait until all
+	 *                               servers responds (for debugging)
+	 * @param string $sl           Sync level in percentage between 0
+	 *                             and 100 or "fast" or "secure".
+	 * @param int $timeout         Max number of seconds to wait
+	 *                             for responses
+	 * @return mixed               PEAR error on error, true otherwise
+	 * @access public
+	 */
+	function verify($token, $use_timestamp=null, $wait_for_all=False,
+			$sl=null, $timeout=null)
+	{
+	  /* Construct parameters string */
+	  $ret = $this->parsePasswordOTP($token);
+	  if (!$ret) {
+	    return PEAR::raiseError('Could not parse Yubikey OTP');
+	  }
+	  $params = array('id'=>$this->_id, 
+			  'otp'=>$ret['otp'],
+			  'nonce'=>md5(uniqid(rand())));
+	  /* Take care of protocol version 2 parameters */
+	  if ($use_timestamp) $params['timestamp'] = 1;
+	  if ($sl) $params['sl'] = $sl;
+	  if ($timeout) $params['timeout'] = $timeout;
+	  ksort($params);
+	  $parameters = '';
+	  foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
+	  $parameters = ltrim($parameters, "&");
+	  
+	  /* Generate signature. */
+	  if($this->_key <> "") {
+	    $signature = base64_encode(hash_hmac('sha1', $parameters,
+						 $this->_key, true));
+	    $signature = preg_replace('/\+/', '%2B', $signature);
+	    $parameters .= '&h=' . $signature;
+	  }
+
+	  /* Generate and prepare request. */
+	  $this->_lastquery=null;
+	  $this->URLreset();
+	  $mh = curl_multi_init();
+	  $ch = array();
+	  while($URLpart=$this->getNextURLpart()) 
+	    {
+	      /* Support https. */
+	      if ($this->_https) {
+		$query = "https://";
+	      } else {
+		$query = "http://";
+	      }
+	      $query .= $URLpart . "?" . $parameters;
+
+	      if ($this->_lastquery) { $this->_lastquery .= " "; }
+	      $this->_lastquery .= $query;
+	      
+	      $handle = curl_init($query);
+	      curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
+	      curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
+	      if (!$this->_httpsverify) {
+		curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
+		curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
+	      }
+	      curl_setopt($handle, CURLOPT_FAILONERROR, true);
+	      /* If timeout is set, we better apply it here as well
+	         in case the validation server fails to follow it. 
+	      */ 
+	      if ($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
+	      curl_multi_add_handle($mh, $handle);
+	      
+	      $ch[(int)$handle] = $handle;
+	    }
+
+	  /* Execute and read request. */
+	  $this->_response=null;
+	  $replay=False;
+	  $valid=False;
+	  do {
+	    /* Let curl do its work. */
+	    while (($mrc = curl_multi_exec($mh, $active))
+		   == CURLM_CALL_MULTI_PERFORM)
+	      ;
+
+	    while ($info = curl_multi_info_read($mh)) {
+	      if ($info['result'] == CURLE_OK) {
+
+		/* We have a complete response from one server. */
+
+		$str = curl_multi_getcontent($info['handle']);
+		$cinfo = curl_getinfo ($info['handle']);
+		
+		if ($wait_for_all) { # Better debug info
+		  $this->_response .= 'URL=' . $cinfo['url'] ."\n"
+		    . $str . "\n";
+		}
+
+		if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
+		  $status = $out[1];
+
+		  /* 
+		   * There are 3 cases.
+		   *
+		   * 1. OTP or Nonce values doesn't match - ignore
+		   * response.
+		   *
+		   * 2. We have a HMAC key.  If signature is invalid -
+		   * ignore response.  Return if status=OK or
+		   * status=REPLAYED_OTP.
+		   *
+		   * 3. Return if status=OK or status=REPLAYED_OTP.
+		   */
+		  if (!preg_match("/otp=".$params['otp']."/", $str) ||
+		      !preg_match("/nonce=".$params['nonce']."/", $str)) {
+		    /* Case 1. Ignore response. */
+		  } 
+		  elseif ($this->_key <> "") {
+		    /* Case 2. Verify signature first */
+		    $rows = explode("\r\n", trim($str));
+		    $response=array();
+		    while (list($key, $val) = each($rows)) {
+		      /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
+		      $val = preg_replace('/=/', '#', $val, 1);
+		      $row = explode("#", $val);
+		      $response[$row[0]] = $row[1];
+		    }
+		    
+		    $parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
+		    sort($parameters);
+		    $check=Null;
+		    foreach ($parameters as $param) {
+		      if (array_key_exists($param, $response)) {
+			if ($check) $check = $check . '&';
+			$check = $check . $param . '=' . $response[$param];
+		      }
+		    }
+
+		    $checksignature =
+		      base64_encode(hash_hmac('sha1', utf8_encode($check),
+					      $this->_key, true));
+
+		    if($response['h'] == $checksignature) {
+		      if ($status == 'REPLAYED_OTP') {
+			if (!$wait_for_all) { $this->_response = $str; }
+			$replay=True;
+		      } 
+		      if ($status == 'OK') {
+			if (!$wait_for_all) { $this->_response = $str; }
+			$valid=True;
+		      }
+		    }
+		  } else {
+		    /* Case 3. We check the status directly */
+		    if ($status == 'REPLAYED_OTP') {
+		      if (!$wait_for_all) { $this->_response = $str; }
+		      $replay=True;
+		    } 
+		    if ($status == 'OK') {
+		      if (!$wait_for_all) { $this->_response = $str; }
+		      $valid=True;
+		    }
+		  }
+		}
+		if (!$wait_for_all && ($valid || $replay)) 
+		  {
+		    /* We have status=OK or status=REPLAYED_OTP, return. */
+		    foreach ($ch as $h) {
+		      curl_multi_remove_handle($mh, $h);
+		      curl_close($h);
+		    }
+		    curl_multi_close($mh);
+		    if ($replay) return PEAR::raiseError('REPLAYED_OTP');
+		    if ($valid) return true;
+		    return PEAR::raiseError($status);
+		  }
+		
+		curl_multi_remove_handle($mh, $info['handle']);
+		curl_close($info['handle']);
+		unset ($ch[(int)$info['handle']]);
+	      }
+	      curl_multi_select($mh);
+	    }
+	  } while ($active);
+
+	  /* Typically this is only reached for wait_for_all=true or
+	   * when the timeout is reached and there is no
+	   * OK/REPLAYED_REQUEST answer (think firewall).
+	   */
+
+	  foreach ($ch as $h) {
+	    curl_multi_remove_handle ($mh, $h);
+	    curl_close ($h);
+	  }
+	  curl_multi_close ($mh);
+	  
+	  if ($replay) return PEAR::raiseError('REPLAYED_OTP');
+	  if ($valid) return true;
+	  return PEAR::raiseError('NO_VALID_ANSWER');
+	}
+}
+?>

+ 0 - 1990
data/web/inc/mailbox.inc.php

@@ -1,1990 +0,0 @@
-<?php
-function mailbox_add_domain($postarray) {
-  // Array elements
-  // domain                 string
-  // description            string
-  // aliases                int
-  // mailboxes              int
-  // maxquota               int
-  // quota                  int
-  // active                 int
-  // relay_all_recipients   int
-  // backupmx               int
-	global $pdo;
-	global $lang;
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	$domain				= idn_to_ascii(strtolower(trim($postarray['domain'])));
-	$description  = $postarray['description'];
-	$aliases			= $postarray['aliases'];
-	$mailboxes    = $postarray['mailboxes'];
-	$maxquota			= $postarray['maxquota'];
-	$quota				= $postarray['quota'];
-
-	if ($maxquota > $quota) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_quota_exceeds_domain_quota'])
-		);
-		return false;
-	}
-
-	isset($postarray['active'])               ? $active = '1'                 : $active = '0';
-	isset($postarray['relay_all_recipients'])	? $relay_all_recipients = '1'   : $relay_all_recipients = '0';
-	isset($postarray['backupmx'])             ? $backupmx = '1'               : $backupmx = '0';
-	isset($postarray['relay_all_recipients']) ? $backupmx = '1'               : true;
-
-	if (!is_valid_domain_name($domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_invalid'])
-		);
-		return false;
-	}
-
-	foreach (array($quota, $maxquota, $mailboxes, $aliases) as $data) {
-		if (!is_numeric($data)) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['object_is_not_numeric'], htmlspecialchars($data))
-			);
-			return false;
-		}
-	}
-
-	try {
-		$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));
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	if ($num_results != 0) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_exists'], htmlspecialchars($domain))
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `created`, `modified`, `active`, `relay_all_recipients`)
-			VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :created, :modified, :active, :relay_all_recipients)");
-		$stmt->execute(array(
-			':domain' => $domain,
-			':description' => $description,
-			':aliases' => $aliases,
-			':mailboxes' => $mailboxes,
-			':maxquota' => $maxquota,
-			':quota' => $quota,
-			':backupmx' => $backupmx,
-			':active' => $active,
-			':created' => date('Y-m-d H:i:s'),
-			':modified' => date('Y-m-d H:i:s'),
-			':relay_all_recipients' => $relay_all_recipients
-		));
-		$_SESSION['return'] = array(
-			'type' => 'success',
-			'msg' => sprintf($lang['success']['domain_added'], htmlspecialchars($domain))
-		);
-	}
-	catch (PDOException $e) {
-    mailbox_delete_domain(array('domain' => $domain));
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-}
-function mailbox_add_alias($postarray) {
-  // Array elements
-  // address  string  (separated by " ", "," ";" "\n") - email address or domain
-  // goto     string  (separated by " ", "," ";" "\n")
-  // active   int
-	global $lang;
-	global $pdo;
-	$addresses  = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['address']));
-	$gotos      = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto']));
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-	if (empty($addresses[0])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['alias_empty'])
-		);
-		return false;
-	}
-
-	if (empty($gotos[0])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['goto_empty'])
-		);
-		return false;
-	}
-
-	foreach ($addresses as $address) {
-		if (empty($address)) {
-			continue;
-		}
-
-		$domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
-		$local_part   = strstr($address, '@', true);
-		$address      = $local_part.'@'.$domain;
-
-		try {
-			$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
-				WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
-			$stmt->execute(array(':domain1' => $domain, ':domain2' => $domain));
-			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-      if ($num_results == 0) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
-        );
-        return false;
-      }
-
-			$stmt = $pdo->prepare("SELECT `address` FROM `alias`
-				WHERE `address`= :address");
-			$stmt->execute(array(':address' => $address));
-			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-      if ($num_results != 0) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address))
-        );
-        return false;
-      }
-
-			$stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
-				WHERE `address`= :address");
-			$stmt->execute(array(':address' => $address));
-			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-      if ($num_results != 0) {
-        $_SESSION['return'] = array(
-          'type' => 'danger',
-          'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($address))
-        );
-        return false;
-      }
-		}
-		catch(PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-
-		if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['alias_invalid'])
-			);
-			return false;
-		}
-
-		if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['access_denied'])
-			);
-			return false;
-		}
-
-		foreach ($gotos as &$goto) {
-			if (empty($goto)) {
-				continue;
-			}
-
-			$goto_domain		= idn_to_ascii(substr(strstr($goto, '@'), 1));
-			$goto_local_part	= strstr($goto, '@', true);
-			$goto				= $goto_local_part.'@'.$goto_domain;
-
-			if (!filter_var($goto, FILTER_VALIDATE_EMAIL) === true) {
-				$_SESSION['return'] = array(
-					'type' => 'danger',
-					'msg' => sprintf($lang['danger']['goto_invalid'])
-				);
-				return false;
-			}
-			if ($goto == $address) {
-				$_SESSION['return'] = array(
-					'type' => 'danger',
-					'msg' => sprintf($lang['danger']['alias_goto_identical'])
-				);
-				return false;
-			}
-		}
-
-		$gotos = array_filter($gotos);
-		$goto = implode(",", $gotos);
-
-		try {
-			$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`)
-				VALUES (:address, :goto, :domain, :created, :modified, :active)");
-
-			if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
-				$stmt->execute(array(
-					':address' => '@'.$domain,
-					':goto' => $goto,
-					':domain' => $domain,
-					':created' => date('Y-m-d H:i:s'),
-					':modified' => date('Y-m-d H:i:s'),
-					':active' => $active
-				));
-			}
-			else {
-				$stmt->execute(array(
-					':address' => $address,
-					':goto' => $goto,
-					':domain' => $domain,
-					':created' => date('Y-m-d H:i:s'),
-					':modified' => date('Y-m-d H:i:s'),
-					':active' => $active
-				));
-			}
-			$_SESSION['return'] = array(
-				'type' => 'success',
-				'msg' => sprintf($lang['success']['alias_added'])
-			);
-		}
-		catch (PDOException $e) {
-      mailbox_delete_alias(array('address' => $address));
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['alias_added'])
-	);
-}
-function mailbox_add_alias_domain($postarray) {
-  // Array elements
-  // active         int
-  // alias_domain   string
-  // target_domain  string
-	global $lang;
-	global $pdo;
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-	$alias_domain     = idn_to_ascii(strtolower(trim($postarray['alias_domain'])));
-	$target_domain    = idn_to_ascii(strtolower(trim($postarray['target_domain'])));
-
-	if (!is_valid_domain_name($alias_domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
-		);
-		return false;
-	}
-
-	if (!is_valid_domain_name($target_domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['target_domain_invalid'])
-		);
-		return false;
-	}
-
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $target_domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-	if ($alias_domain == $target_domain) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['aliasd_targetd_identical'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
-			WHERE `domain`= :target_domain");
-		$stmt->execute(array(':target_domain' => $target_domain));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-    if ($num_results == 0) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['targetd_not_found'])
-      );
-      return false;
-    }
-
-		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
-			UNION
-			SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain_in_domain");
-		$stmt->execute(array(':alias_domain' => $alias_domain, ':alias_domain_in_domain' => $alias_domain));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-    if ($num_results != 0) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['aliasd_exists'])
-      );
-      return false;
-    }
-  }
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `created`, `modified`, `active`)
-			VALUES (:alias_domain, :target_domain, :created, :modified, :active)");
-		$stmt->execute(array(
-			':alias_domain' => $alias_domain,
-			':target_domain' => $target_domain,
-			':created' => date('Y-m-d H:i:s'),
-			':modified' => date('Y-m-d H:i:s'),
-			':active' => $active
-		));
-		$_SESSION['return'] = array(
-			'type' => 'success',
-			'msg' => sprintf($lang['success']['aliasd_added'], htmlspecialchars($alias_domain))
-		);
-	}
-	catch (PDOException $e) {
-    mailbox_delete_alias_domain(array('alias_domain' => $alias_domain));
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-}
-function mailbox_add_mailbox($postarray) {
-  // Array elements
-  // active             int
-  // local_part         string
-  // domain             string
-  // name               string    (username if empty)
-  // password           string
-  // password2          string
-  // quota              int       (MiB)
-  // active             int
-
-	global $pdo;
-	global $lang;
-	$local_part   = strtolower(trim($postarray['local_part']));
-	$domain       = idn_to_ascii(strtolower(trim($postarray['domain'])));
-  $username     = $local_part . '@' . $domain;
-	if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_invalid'])
-		);
-		return false;
-	}
-	if (empty($postarray['local_part'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_invalid'])
-		);
-		return false;
-	}
-	$password     = $postarray['password'];
-	$password2    = $postarray['password2'];
-	$name         = $postarray['name'];
-  $quota_m			= filter_var($postarray['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
-
-	if (empty($name)) {
-		$name = $local_part;
-	}
-
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-
-	$quota_b		= ($quota_m * 1048576);
-	$maildir		= $domain."/".$local_part."/";
-
-	if (!is_valid_domain_name($domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_invalid'])
-		);
-		return false;
-	}
-
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("SELECT `mailboxes`, `maxquota`, `quota` FROM `domain`
-			WHERE `domain` = :domain");
-		$stmt->execute(array(':domain' => $domain));
-		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
-
-		$stmt = $pdo->prepare("SELECT 
-			COUNT(*) as count,
-			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota`
-				FROM `mailbox`
-					WHERE `domain` = :domain");
-		$stmt->execute(array(':domain' => $domain));
-		$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
-
-		$stmt = $pdo->prepare("SELECT `local_part` FROM `mailbox` WHERE `local_part` = :local_part and `domain`= :domain");
-		$stmt->execute(array(':local_part' => $local_part, ':domain' => $domain));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-    if ($num_results != 0) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
-      );
-      return false;
-    }
-
-		$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE address= :username");
-		$stmt->execute(array(':username' => $username));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-    if ($num_results != 0) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['is_alias'], htmlspecialchars($username))
-      );
-      return false;
-    }
-
-		$stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :username");
-		$stmt->execute(array(':username' => $username));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-    if ($num_results != 0) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($username))
-      );
-      return false;
-    }
-
-		$stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain");
-		$stmt->execute(array(':domain' => $domain));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-    if ($num_results == 0) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
-      );
-      return false;
-    }
-  }
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-	if (!is_numeric($quota_m) || $quota_m == "0") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['quota_not_0_not_numeric'])
-		);
-		return false;
-	}
-
-	if (!empty($password) && !empty($password2)) {
-		if ($password != $password2) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['password_mismatch'])
-			);
-			return false;
-		}
-		$password_hashed = hash_password($password);
-	}
-	else {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['password_empty'])
-		);
-		return false;
-	}
-
-	if ($MailboxData['count'] >= $DomainData['mailboxes']) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['max_mailbox_exceeded'], $MailboxData['count'], $DomainData['mailboxes'])
-		);
-		return false;
-	}
-
-	if ($quota_m > $DomainData['maxquota']) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_quota_exceeded'], $DomainData['maxquota'])
-		);
-		return false;
-	}
-
-	if (($MailboxData['quota'] + $quota_m) > $DomainData['quota']) {
-		$quota_left_m = ($DomainData['quota'] - $MailboxData['quota']);
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_quota_left_exceeded'], $quota_left_m)
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`) 
-			VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :created, :modified, :active)");
-		$stmt->execute(array(
-			':username' => $username,
-			':password_hashed' => $password_hashed,
-			':name' => $name,
-			':maildir' => $maildir,
-			':quota_b' => $quota_b,
-			':local_part' => $local_part,
-			':domain' => $domain,
-			':created' => date('Y-m-d H:i:s'),
-			':modified' => date('Y-m-d H:i:s'),
-			':active' => $active
-		));
-
-		$stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
-			VALUES (:username, '0', '0')");
-		$stmt->execute(array(':username' => $username));
-
-		$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`)
-			VALUES (:username1, :username2, :domain, :created, :modified, :active)");
-		$stmt->execute(array(
-			':username1' => $username,
-			':username2' => $username,
-			':domain' => $domain,
-			':created' => date('Y-m-d H:i:s'),
-			':modified' => date('Y-m-d H:i:s'),
-			':active' => $active
-		));
-
-		$_SESSION['return'] = array(
-			'type' => 'success',
-			'msg' => sprintf($lang['success']['mailbox_added'], htmlspecialchars($username))
-		);
-	}
-	catch (PDOException $e) {
-    mailbox_delete_mailbox(array('address' => $username));
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-}
-function mailbox_edit_alias_domain($postarray) {
-  // Array elements
-  // active             int
-  // alias_domain_now   string
-  // alias_domain       string
-	global $lang;
-	global $pdo;
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-	$alias_domain       = idn_to_ascii(strtolower(trim($postarray['alias_domain'])));
-	$alias_domain_now   = strtolower(trim($postarray['alias_domain_now']));
-	if (!is_valid_domain_name($alias_domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
-		);
-		return false;
-	}
-
-	if (!is_valid_domain_name($alias_domain_now)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
-				WHERE `alias_domain`= :alias_domain_now");
-		$stmt->execute(array(':alias_domain_now' => $alias_domain_now));
-		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
-		WHERE `target_domain`= :alias_domain");
-		$stmt->execute(array(':alias_domain' => $alias_domain));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	if ($num_results != 0) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['aliasd_targetd_identical'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("UPDATE `alias_domain` SET
-      `alias_domain` = :alias_domain,
-      `active` = :active,
-      `modified` = :modified,
-        WHERE `alias_domain` = :alias_domain_now");
-		$stmt->execute(array(
-			':alias_domain' => $alias_domain,
-      ':modified' => date('Y-m-d H:i:s'),
-			':alias_domain_now' => $alias_domain_now,
-			':active' => $active
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['aliasd_modified'], htmlspecialchars($alias_domain))
-	);
-}
-function mailbox_edit_alias($postarray) {
-  // Array elements
-  // address            string
-  // goto               string    (separated by " ", "," ";" "\n") - email address or domain
-  // active             int
-	global $lang;
-	global $pdo;
-	$address      = $postarray['address'];
-	$domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
-	$local_part   = strstr($address, '@', true);
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	if (empty($postarray['goto'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['goto_empty'])
-		);
-		return false;
-	}
-	$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto']));
-	foreach ($gotos as &$goto) {
-		if (empty($goto)) {
-			continue;
-		}
-		if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' =>sprintf($lang['danger']['goto_invalid'])
-			);
-			return false;
-		}
-		if ($goto == $address) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['alias_goto_identical'])
-			);
-			return false;
-		}
-	}
-	$gotos = array_filter($gotos);
-	$goto = implode(",", $gotos);
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-	if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['alias_invalid'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("UPDATE `alias` SET
-      `goto` = :goto,
-      `active`= :active,
-      `modified` = :modified,
-        WHERE `address` = :address");
-		$stmt->execute(array(
-			':goto' => $goto,
-			':active' => $active,
-			':address' => $address,
-      ':modified' => date('Y-m-d H:i:s'),
-		));
-		$_SESSION['return'] = array(
-			'type' => 'success',
-		'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars($address))
-		);
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-}
-function mailbox_edit_domain($postarray) {
-  // Array elements
-  // domain                 string
-  // description            string
-  // active                 int
-  // relay_all_recipients   int
-  // backupmx               int
-  // aliases                float
-  // mailboxes              float
-  // maxquota               float
-  // quota                  float     (Byte)
-  // active                 int
-
-	global $lang;
-	global $pdo;
-  
-  $domain       = idn_to_ascii($postarray['domain']);
-	if (!is_valid_domain_name($domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_invalid'])
-		);
-		return false;
-	}
-
-	if ($_SESSION['mailcow_cc_role'] == "domainadmin" && 	hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-    $description  = $postarray['description'];
-    isset($postarray['active']) ? $active = '1' : $active = '0';
-    try {
-      $stmt = $pdo->prepare("UPDATE `domain` SET 
-      `modified`= :modified,
-      `description` = :description
-        WHERE `domain` = :domain");
-      $stmt->execute(array(
-        ':modified' => date('Y-m-d H:i:s'),
-        ':description' => $description,
-        ':domain' => $domain
-      ));
-      $_SESSION['return'] = array(
-        'type' => 'success',
-        'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars($domain))
-      );
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-  elseif ($_SESSION['mailcow_cc_role'] == "admin") {
-    $description  = $postarray['description'];
-    isset($postarray['active']) ? $active = '1' : $active = '0';
-    $aliases		= filter_var($postarray['aliases'], FILTER_SANITIZE_NUMBER_FLOAT);
-    $mailboxes  = filter_var($postarray['mailboxes'], FILTER_SANITIZE_NUMBER_FLOAT);
-    $maxquota		= filter_var($postarray['maxquota'], FILTER_SANITIZE_NUMBER_FLOAT);
-    $quota			= filter_var($postarray['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
-    isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0';
-    isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0';
-    isset($postarray['relay_all_recipients']) ? $backupmx = '1' : true;
-    try {
-      // GET MAILBOX DATA
-      $stmt = $pdo->prepare("SELECT 
-          COUNT(*) AS count,
-          MAX(COALESCE(ROUND(`quota`/1048576), 0)) AS `maxquota`,
-          COALESCE(ROUND(SUM(`quota`)/1048576), 0) AS `quota`
-            FROM `mailbox`
-              WHERE domain= :domain");
-      $stmt->execute(array(':domain' => $domain));
-      $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
-      // GET ALIAS DATA
-      $stmt = $pdo->prepare("SELECT COUNT(*) AS `count` FROM `alias`
-          WHERE domain = :domain
-          AND address NOT IN (
-            SELECT `username` FROM `mailbox`
-          )");
-      $stmt->execute(array(':domain' => $domain));
-      $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
-    }
-    catch(PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-
-    if ($maxquota > $quota) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['mailbox_quota_exceeds_domain_quota'])
-      );
-      return false;
-    }
-
-    if ($MailboxData['maxquota'] > $maxquota) {
-      echo $MailboxData['maxquota'];
-      die();
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['max_quota_in_use'], $MailboxData['maxquota'])
-      );
-      return false;
-    }
-
-    if ($MailboxData['quota'] > $quota) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['domain_quota_m_in_use'], $MailboxData['quota'])
-      );
-      return false;
-    }
-
-    if ($MailboxData['count'] > $mailboxes) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['mailboxes_in_use'], $MailboxData['count'])
-      );
-      return false;
-    }
-
-    if ($AliasData['count'] > $aliases) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['aliases_in_use'], $AliasData['count'])
-      );
-      return false;
-    }
-    try {
-      $stmt = $pdo->prepare("UPDATE `domain` SET 
-      `modified`= :modified,
-      `relay_all_recipients` = :relay_all_recipients,
-      `backupmx` = :backupmx,
-      `active` = :active,
-      `quota` = :quota,
-      `maxquota` = :maxquota,
-      `modified` = :modified,
-      `mailboxes` = :mailboxes,
-      `aliases` = :aliases,
-      `description` = :description
-        WHERE `domain` = :domain");
-      $stmt->execute(array(
-        ':relay_all_recipients' => $relay_all_recipients,
-        ':backupmx' => $backupmx,
-        ':active' => $active,
-        ':quota' => $quota,
-        ':maxquota' => $maxquota,
-        ':modified' => date('Y-m-d H:i:s'),
-        ':mailboxes' => $mailboxes,
-        ':aliases' => $aliases,
-        ':modified' => date('Y-m-d H:i:s'),
-        ':description' => $description,
-        ':domain' => $domain
-      ));
-      $_SESSION['return'] = array(
-        'type' => 'success',
-        'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars($domain))
-      );
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-}
-function mailbox_edit_mailbox($postarray) {
-	global $lang;
-	global $pdo;
-	isset($postarray['active']) ? $active = '1' : $active = '0';
-	if (!filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['username_invalid'])
-		);
-		return false;
-	}
-	$quota_m      = $postarray['quota'];
-	$quota_b      = $quota_m*1048576;
-	$username     = $postarray['username'];
-	$name         = $postarray['name'];
-	$password     = $postarray['password'];
-	$password2    = $postarray['password2'];
-
-	try {
-		$stmt = $pdo->prepare("SELECT `domain`
-			FROM `mailbox`
-				WHERE username = :username");
-		$stmt->execute(array(':username' => $username));
-		$MailboxData1 = $stmt->fetch(PDO::FETCH_ASSOC);
-
-		$stmt = $pdo->prepare("SELECT 
-			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota_m_now`
-				FROM `mailbox`
-					WHERE `username` = :username");
-		$stmt->execute(array(':username' => $username));
-		$MailboxData2 = $stmt->fetch(PDO::FETCH_ASSOC);
-
-		$stmt = $pdo->prepare("SELECT 
-			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota_m_in_use`
-				FROM `mailbox`
-					WHERE `domain` = :domain");
-		$stmt->execute(array(':domain' => $MailboxData1['domain']));
-		$MailboxData3 = $stmt->fetch(PDO::FETCH_ASSOC);
-
-		$stmt = $pdo->prepare("SELECT `quota`, `maxquota`
-			FROM `domain`
-				WHERE `domain` = :domain");
-		$stmt->execute(array(':domain' => $MailboxData1['domain']));
-		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $MailboxData1['domain'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	if (!is_numeric($quota_m) || $quota_m == "0") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['quota_not_0_not_numeric'], htmlspecialchars($quota_m))
-		);
-		return false;
-	}
-	if ($quota_m > $DomainData['maxquota']) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_quota_exceeded'], $DomainData['maxquota'])
-		);
-		return false;
-	}
-	if (($MailboxData3['quota_m_in_use'] - $MailboxData2['quota_m_now'] + $quota_m) > $DomainData['quota']) {
-		$quota_left_m = ($DomainData['quota'] - $MailboxData3['quota_m_in_use'] + $MailboxData2['quota_m_now']);
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['mailbox_quota_left_exceeded'], $quota_left_m)
-		);
-		return false;
-	}
-
-  // Get sender_acl items set by admin
-  $sender_acl_admin = array_merge(
-    mailbox_get_sender_acl_handles($username)['sender_acl_domains']['ro'],
-    mailbox_get_sender_acl_handles($username)['sender_acl_addresses']['ro']
-  );
-
-  // Get sender_acl items from POST array
-  (isset($postarray['sender_acl'])) ? $sender_acl_domain_admin = $postarray['sender_acl'] : $sender_acl_domain_admin = array();
-
-	if (!empty($sender_acl_domain_admin) || !empty($sender_acl_admin)) {
-    // Check items in POST array
-		foreach ($sender_acl_domain_admin as $sender_acl) {
-			if (!filter_var($sender_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($sender_acl, '@'))) {
-					$_SESSION['return'] = array(
-						'type' => 'danger',
-						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
-					);
-					return false;
-			}
-      if (is_valid_domain_name(ltrim($sender_acl, '@'))) {
-        if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($sender_acl, '@'))) {
-					$_SESSION['return'] = array(
-						'type' => 'danger',
-						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
-					);
-					return false;
-        }
-      }
-			if (filter_var($sender_acl, FILTER_VALIDATE_EMAIL)) {
-        if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $sender_acl)) {
-					$_SESSION['return'] = array(
-						'type' => 'danger',
-						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
-					);
-					return false;
-        }
-      }
-    }
-
-    // Merge both arrays
-    $sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
-
-    try {
-      $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
-      $stmt->execute(array(
-        ':username' => $username
-      ));
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-
-		foreach ($sender_acl_merged as $sender_acl) {
-      $domain = ltrim($sender_acl, '@');
-      if (is_valid_domain_name($domain)) {
-        $sender_acl = '@' . $domain;
-      }
-			try {
-				$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
-					VALUES (:sender_acl, :username)");
-				$stmt->execute(array(
-					':sender_acl' => $sender_acl,
-					':username' => $username
-				));
-			}
-			catch (PDOException $e) {
-				$_SESSION['return'] = array(
-					'type' => 'danger',
-					'msg' => 'MySQL: '.$e
-				);
-				return false;
-			}
-		}
-	}
-  else {
-    try {
-      $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
-      $stmt->execute(array(
-        ':username' => $username
-      ));
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-	if (!empty($password) && !empty($password2)) {
-		if ($password != $password2) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => sprintf($lang['danger']['password_mismatch'])
-			);
-			return false;
-		}
-		$password_hashed = hash_password($password);
-		try {
-			$stmt = $pdo->prepare("UPDATE `alias` SET
-					`modified` = :modified,
-					`active` = :active
-						WHERE `address` = :address");
-			$stmt->execute(array(
-				':address' => $username,
-				':modified' => date('Y-m-d H:i:s'),
-				':active' => $active
-			));
-			$stmt = $pdo->prepare("UPDATE `mailbox` SET
-					`modified` = :modified,
-					`active` = :active,
-					`password` = :password_hashed,
-					`name`= :name,
-					`quota` = :quota_b
-						WHERE `username` = :username");
-			$stmt->execute(array(
-				':modified' => date('Y-m-d H:i:s'),
-				':password_hashed' => $password_hashed,
-				':active' => $active,
-				':name' => $name,
-				':quota_b' => $quota_b,
-				':username' => $username
-			));
-			$_SESSION['return'] = array(
-				'type' => 'success',
-				'msg' => sprintf($lang['success']['mailbox_modified'], $username)
-			);
-			return true;
-		}
-		catch (PDOException $e) {
-			$_SESSION['return'] = array(
-				'type' => 'danger',
-				'msg' => 'MySQL: '.$e
-			);
-			return false;
-		}
-	}
-	try {
-		$stmt = $pdo->prepare("UPDATE `alias` SET
-				`modified` = :modified,
-				`active` = :active
-					WHERE `address` = :address");
-		$stmt->execute(array(
-			':address' => $username,
-			':modified' => date('Y-m-d H:i:s'),
-			':active' => $active
-		));
-		$stmt = $pdo->prepare("UPDATE `mailbox` SET
-				`modified` = :modified,
-				`active` = :active,
-				`name`= :name,
-				`quota` = :quota_b
-					WHERE `username` = :username");
-		$stmt->execute(array(
-			':active' => $active,
-			':modified' => date('Y-m-d H:i:s'),
-			':name' => $name,
-			':quota_b' => $quota_b,
-			':username' => $username
-		));
-		$_SESSION['return'] = array(
-			'type' => 'success',
-			'msg' => sprintf($lang['success']['mailbox_modified'], $username)
-		);
-		return true;
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-}
-function mailbox_get_mailboxes($domain = null) {
-	global $lang;
-	global $pdo;
-  $mailboxes = array();
-	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-    try {
-      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `domain` != 'ALL' AND `domain` = :domain");
-      $stmt->execute(array(
-        ':domain' => $domain,
-      ));
-      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-      while($row = array_shift($rows)) {
-        $mailboxes[] = $row['username'];
-      }
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-  else {
-    try {
-      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
-      $stmt->execute(array(
-        ':username' => $_SESSION['mailcow_cc_username'],
-        ':role' => $_SESSION['mailcow_cc_role'],
-      ));
-      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-      while($row = array_shift($rows)) {
-        $mailboxes[] = $row['username'];
-      }
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-  return $mailboxes;
-}
-function mailbox_get_alias_domains($domain = null) {
-  // Get all domains assigned to mailcow_cc_username or domain, if set
-  // Domain admin needs to be active
-  // Domain does not need to be active
-	global $lang;
-	global $pdo;
-  $aliasdomains = array();
-	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-  }
-  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-    try {
-      $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain");
-      $stmt->execute(array(
-        ':domain' => $domain,
-      ));
-      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-      while($row = array_shift($rows)) {
-        $aliasdomains[] = $row['alias_domain'];
-      }
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-	else {
-    try {
-      $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
-      $stmt->execute(array(
-        ':username' => $_SESSION['mailcow_cc_username'],
-        ':role' => $_SESSION['mailcow_cc_role'],
-      ));
-      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-      while($row = array_shift($rows)) {
-        $aliasdomains[] = $row['alias_domain'];
-      }
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => 'MySQL: '.$e
-      );
-      return false;
-    }
-  }
-  return $aliasdomains;
-}
-function mailbox_get_aliases($domain) {
-	global $lang;
-	global $pdo;
-  $aliases = array();
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-  try {
-    $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
-    $stmt->execute(array(
-      ':domain' => $domain,
-    ));
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while($row = array_shift($rows)) {
-      $aliases[] = $row['address'];
-    }
-  }
-  catch (PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-  return $aliases;
-}
-function mailbox_get_alias_details($address) {
-	global $lang;
-	global $pdo;
-  $aliasdata = array();
-  try {
-    $stmt = $pdo->prepare("SELECT
-      `domain`,
-      `goto`,
-      `address`,
-      `active` as `active_int`,
-      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
-      `created`,
-      `modified`
-        FROM `alias`
-            WHERE `address` = :address AND `address` != `goto`");
-    $stmt->execute(array(
-      ':address' => $address,
-    ));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
-    $aliasdata['domain'] = $row['domain'];
-    $aliasdata['goto'] = $row['goto'];
-    $aliasdata['address'] = $row['address'];
-    (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
-    $aliasdata['active'] = $row['active'];
-    $aliasdata['active_int'] = $row['active_int'];
-    $aliasdata['created'] = $row['created'];
-    $aliasdata['modified'] = $row['modified'];
-    if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
-      $_SESSION['return'] = array(
-        'type' => 'danger',
-        'msg' => sprintf($lang['danger']['access_denied'])
-      );
-      return false;
-    }
-  }
-  catch (PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-  return $aliasdata;
-}
-function mailbox_get_alias_domain_details($aliasdomain) {
-	global $lang;
-	global $pdo;
-  $aliasdomaindata = array();
-  try {
-    $stmt = $pdo->prepare("SELECT
-      `alias_domain`,
-      `target_domain`,
-      `active` AS `active_int`,
-      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
-      `created`,
-      `modified`
-        FROM `alias_domain`
-            WHERE `alias_domain` = :aliasdomain");
-    $stmt->execute(array(
-      ':aliasdomain' => $aliasdomain,
-    ));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
-    $aliasdomaindata['alias_domain'] = $row['alias_domain'];
-    $aliasdomaindata['target_domain'] = $row['target_domain'];
-    $aliasdomaindata['active'] = $row['active'];
-    $aliasdomaindata['active_int'] = $row['active_int'];
-    $aliasdomaindata['created'] = $row['created'];
-    $aliasdomaindata['modified'] = $row['modified'];
-  }
-  catch (PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-  if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdomaindata['target_domain'])) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  return $aliasdomaindata;
-}
-function mailbox_get_domains() {
-  // Get all domains assigned to mailcow_cc_username
-  // Domain admin needs to be active
-  // Domain does not need to be active
-	global $lang;
-	global $pdo;
-
-  try {
-    $domains = array();
-    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
-      WHERE (`domain` IN (
-        SELECT `domain` from `domain_admins`
-          WHERE (`active`='1' AND `username` = :username))
-        )
-        OR ('admin'= :role)
-        AND `domain` != 'ALL'");
-    $stmt->execute(array(
-      ':username' => $_SESSION['mailcow_cc_username'],
-      ':role' => $_SESSION['mailcow_cc_role'],
-    ));
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while($row = array_shift($rows)) {
-      $domains[] = $row['domain'];
-    }
-  }
-  catch (PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-  return $domains;
-}
-function mailbox_get_domain_details($domain) {
-	global $lang;
-	global $pdo;
-
-  $domaindata = array();
-	$domain = idn_to_ascii(strtolower(trim($domain)));
-
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-  try {
-    $stmt = $pdo->prepare("SELECT 
-        `domain`,
-        `description`,
-        `aliases`,
-        `mailboxes`, 
-        `maxquota`,
-        `quota`,
-        `relay_all_recipients` as `relay_all_recipients_int`,
-        `backupmx` as `backupmx_int`,
-        `active` as `active_int`,
-        CASE `relay_all_recipients` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `relay_all_recipients`,
-        CASE `backupmx` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `backupmx`,
-        CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
-          FROM `domain` WHERE `domain`= :domain");
-    $stmt->execute(array(
-      ':domain' => $domain,
-    ));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
-    $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `domain` = :domain");
-    $stmt->execute(array(':domain' => $row['domain']));
-    $MailboxDataDomain	= $stmt->fetch(PDO::FETCH_ASSOC);
-
-    $domaindata['max_new_mailbox_quota']	= ($row['quota'] * 1048576) - $MailboxDataDomain['in_use'];
-    if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
-      $domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
-    }
-    $domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use'];
-    $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
-    $domaindata['mboxes_left'] = $row['mailboxes']	- $MailboxDataDomain['count'];
-    $domaindata['domain_name'] = $row['domain'];
-    $domaindata['description'] = $row['description'];
-    $domaindata['max_num_aliases_for_domain'] = $row['aliases'];
-    $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
-    $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576;
-    $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576;
-    $domaindata['backupmx'] = $row['backupmx'];
-    $domaindata['backupmx_int'] = $row['backupmx_int'];
-    $domaindata['active'] = $row['active'];
-    $domaindata['active_int'] = $row['active_int'];
-    $domaindata['relay_all_recipients'] = $row['relay_all_recipients'];
-    $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients_int'];
-
-    $stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias`
-      WHERE `domain`= :domain
-        AND `address` NOT IN (
-          SELECT `username` FROM `mailbox`
-        )");
-    $stmt->execute(array(
-      ':domain' => $domain,
-    ));
-    $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
-    (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0";
-  }
-  catch (PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-
-  return $domaindata;
-}
-function mailbox_get_mailbox_details($mailbox) {
-	global $lang;
-	global $pdo;
-  $mailboxdata = array();
-  try {
-    $stmt = $pdo->prepare("SELECT
-        `domain`.`backupmx`,
-        `mailbox`.`username`,
-        `mailbox`.`name`,
-        `mailbox`.`active` AS `active_int`,
-        CASE `mailbox`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
-        `mailbox`.`domain`,
-        `mailbox`.`quota`,
-        `quota2`.`bytes`,
-        `quota2`.`messages`
-          FROM `mailbox`, `quota2`, `domain`
-            WHERE `mailbox`.`username` = `quota2`.`username` AND `domain`.`domain` = `mailbox`.`domain` AND `mailbox`.`username` = :mailbox");
-    $stmt->execute(array(
-      ':mailbox' => $mailbox,
-    ));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
-
-    $stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM  `domain` WHERE `domain` = :domain");
-    $stmt->execute(array(':domain' => $row['domain']));
-    $DomainQuota  = $stmt->fetch(PDO::FETCH_ASSOC);
-
-    $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `domain` = :domain AND `username` != :username");
-    $stmt->execute(array(':domain' => $row['domain'], ':username' => $row['username']));
-    $MailboxUsage	= $stmt->fetch(PDO::FETCH_ASSOC);
-
-    $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
-    if ($mailboxdata['max_new_quota'] > ($DomainQuota['maxquota'] * 1048576)) {
-      $mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
-    }
-
-    $mailboxdata['username'] = $row['username'];
-    $mailboxdata['is_relayed'] = $row['backupmx'];
-    $mailboxdata['name'] = $row['name'];
-    $mailboxdata['active'] = $row['active'];
-    $mailboxdata['active_int'] = $row['active_int'];
-    $mailboxdata['domain'] = $row['domain'];
-    $mailboxdata['quota'] = $row['quota'];
-    $mailboxdata['quota_used'] = intval($row['bytes']);
-    $mailboxdata['percent_in_use'] = round((intval($row['bytes']) / intval($row['quota'])) * 100);
-    $mailboxdata['messages'] = $row['messages'];
-    if ($mailboxdata['percent_in_use'] >= 90) {
-      $mailboxdata['percent_class'] = "danger";
-    }
-    elseif ($mailboxdata['percent_in_use'] >= 75) {
-      $mailboxdata['percent_class'] = "warning";
-    }
-    else {
-      $mailboxdata['percent_class'] = "success";
-    }
-  }
-  catch (PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-  if (!isset($mailboxdata['domain']) ||
-    (isset($mailboxdata['domain']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $mailboxdata['domain']))) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  
-  return $mailboxdata;
-}
-function mailbox_delete_domain($postarray) {
-	global $lang;
-	global $pdo;
-	$domain = $postarray['domain'];
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	if (!is_valid_domain_name($domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_invalid'])
-		);
-		return false;
-	}
-	$domain	= strtolower(trim($domain));
-
-
-	try {
-		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
-			WHERE `domain` = :domain");
-		$stmt->execute(array(':domain' => $domain));
-		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	if ($num_results != 0 || !empty($num_results)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_not_empty'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `domain` WHERE `domain` = :domain");
-		$stmt->execute(array(
-			':domain' => $domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain");
-		$stmt->execute(array(
-			':domain' => $domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :domain");
-		$stmt->execute(array(
-			':domain' => $domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `target_domain` = :domain");
-		$stmt->execute(array(
-			':domain' => $domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `domain` = :domain");
-		$stmt->execute(array(
-			':domain' => $domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` LIKE :domain");
-		$stmt->execute(array(
-			':domain' => '%@'.$domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :domain");
-		$stmt->execute(array(
-			':domain' => '%@'.$domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `address` = :domain");
-		$stmt->execute(array(
-			':domain' => '%@'.$domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :domain");
-		$stmt->execute(array(
-			':domain' => '%@'.$domain,
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['domain_removed'], htmlspecialchars($domain))
-	);
-	return true;
-}
-function mailbox_delete_alias($postarray) {
-	global $lang;
-	global $pdo;
-	$address		= $postarray['address'];
-	$local_part		= strstr($address, '@', true);
-  $domain = mailbox_get_alias_details($address)['domain'];
-	try {
-		$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address");
-		$stmt->execute(array(':address' => $address));
-		$gotos = $stmt->fetch(PDO::FETCH_ASSOC);
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	$goto_array = explode(',', $gotos['goto']);
-
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)");
-		$stmt->execute(array(
-			':address' => $address
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars($address))
-	);
-
-}
-function mailbox_delete_alias_domain($postarray) {
-	global $lang;
-	global $pdo;
-	if (!is_valid_domain_name($postarray['alias_domain'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['domain_invalid'])
-		);
-		return false;
-	}
-	$alias_domain = $postarray['alias_domain'];
-	try {
-		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
-			WHERE `alias_domain`= :alias_domain");
-		$stmt->execute(array(':alias_domain' => $alias_domain));
-		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
-	}
-	catch(PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
-		$stmt->execute(array(
-			':alias_domain' => $alias_domain,
-		));
-		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :alias_domain");
-		$stmt->execute(array(
-			':alias_domain' => $alias_domain,
-		));
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['alias_domain_removed'], htmlspecialchars($alias_domain))
-	);
-}
-function mailbox_delete_mailbox($postarray) {
-	global $lang;
-	global $pdo;
-	$username	= $postarray['username'];
-  $domain = mailbox_get_mailbox_details($username)['domain'];
-	if (!filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => sprintf($lang['danger']['access_denied'])
-		);
-		return false;
-	}
-
-	try {
-		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `username` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `user2` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username");
-		$stmt->execute(array(
-			':username' => $username
-		));
-		$stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias`
-				WHERE `goto` LIKE :username");
-		$stmt->execute(array(':username' => '%'.$username.'%'));
-		$GotoData = $stmt->fetchAll(PDO::FETCH_ASSOC);
-		foreach ($GotoData as $gotos) {
-			$goto_exploded = explode(',', $gotos['goto']);
-			if (($key = array_search($username, $goto_exploded)) !== false) {
-				unset($goto_exploded[$key]);
-			}
-			$gotos_rebuild = implode(',', $goto_exploded);
-			$stmt = $pdo->prepare("UPDATE `alias` SET
-        `goto` = :goto,
-        `modified` = :modified,
-          WHERE `address` = :address");
-			$stmt->execute(array(
-				':goto' => $gotos_rebuild,
-        ':modified' => date('Y-m-d H:i:s'),
-				':address' => $gotos['address']
-			));
-		}
-	}
-	catch (PDOException $e) {
-		$_SESSION['return'] = array(
-			'type' => 'danger',
-			'msg' => 'MySQL: '.$e
-		);
-		return false;
-	}
-	$_SESSION['return'] = array(
-		'type' => 'success',
-		'msg' => sprintf($lang['success']['mailbox_removed'], htmlspecialchars($username))
-	);
-}
-function mailbox_get_sender_acl_handles($mailbox) {
-	global $pdo;
-	global $lang;
-	if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-	}
-
-  $data['sender_acl_domains']['ro']               = array();
-  $data['sender_acl_domains']['rw']               = array();
-  $data['sender_acl_domains']['selectable']       = array();
-  $data['sender_acl_addresses']['ro']             = array();
-  $data['sender_acl_addresses']['rw']             = array();
-  $data['sender_acl_addresses']['selectable']     = array();
-  $data['fixed_sender_aliases']                   = array();
-  
-  try {
-    $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` = :goto AND `address` NOT LIKE '@%'");
-    $stmt->execute(array(':goto' => $mailbox));
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while ($row = array_shift($rows)) {
-      $data['fixed_sender_aliases'][] = $row['address'];
-    }
-
-    // Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
-    // Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
-    $stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `send_as` LIKE '@%'");
-    $stmt->execute(array(':logged_in_as' => $mailbox));
-    $domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while ($domain_row = array_shift($domain_rows)) {
-      if (is_valid_domain_name($domain_row['send_as']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
-        $data['sender_acl_domains']['ro'][] = $domain_row['send_as'];
-        continue;
-      }
-      if (is_valid_domain_name($domain_row['send_as']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
-        $data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
-        continue;
-      }
-    }
-
-    $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `send_as` NOT LIKE '@%'");
-    $stmt->execute(array(':logged_in_as' => $mailbox));
-    $address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while ($address_row = array_shift($address_rows)) {
-      if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
-        $data['sender_acl_addresses']['ro'][] = $address_row['send_as'];
-        continue;
-      }
-      if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
-        $data['sender_acl_addresses']['rw'][] = $address_row['send_as'];
-        continue;
-      }
-    }
-
-    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
-      WHERE `domain` NOT IN (
-        SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` 
-          WHERE `logged_in_as` = :logged_in_as
-            AND `send_as` LIKE '@%')");
-    $stmt->execute(array(
-      ':logged_in_as' => $mailbox,
-    ));
-    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while ($row = array_shift($rows)) {
-      if (is_valid_domain_name($row['domain']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) {
-        $data['sender_acl_domains']['selectable'][] = $row['domain'];
-      }
-    }
-
-    $stmt = $pdo->prepare("SELECT `address` FROM `alias`
-      WHERE `goto` != :goto
-        AND `address` NOT IN (
-          SELECT `send_as` FROM `sender_acl` 
-            WHERE `logged_in_as` = :logged_in_as
-              AND `send_as` NOT LIKE '@%')");
-    $stmt->execute(array(
-      ':logged_in_as' => $mailbox,
-      ':goto' => $mailbox
-    ));
-    while ($row = array_shift($rows)) {
-      if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
-        $data['sender_acl_addresses']['selectable'][] = $row['address'];
-      }
-    }
-  }
-  catch(PDOException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'MySQL: '.$e
-    );
-    return false;
-  }
-  return $data;
-}

+ 13 - 2
data/web/inc/prerequisites.inc.php

@@ -17,11 +17,21 @@ if (isset($_POST["logout"])) {
 }
 }
 
 
 require_once 'inc/vars.inc.php';
 require_once 'inc/vars.inc.php';
-
 if (file_exists('./inc/vars.local.inc.php')) {
 if (file_exists('./inc/vars.local.inc.php')) {
 	include_once 'inc/vars.local.inc.php';
 	include_once 'inc/vars.local.inc.php';
 }
 }
 
 
+// Yubi OTP API
+if (!empty($YUBI_API['ID']) && !empty($YUBI_API['KEY'])) {
+  require_once 'inc/lib/Yubico.php';
+  $yubi = new Auth_Yubico($YUBI_API['ID'], $YUBI_API['KEY']);
+}
+// U2F API
+require_once 'inc/lib/U2F.php';
+$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://";
+$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']);
+
+// PDO
 $dsn = "$database_type:host=$database_host;dbname=$database_name";
 $dsn = "$database_type:host=$database_host;dbname=$database_name";
 $opt = [
 $opt = [
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
@@ -36,6 +46,7 @@ catch (PDOException $e) {
 <center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>🐮 Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/>  <?=$e->getMessage();?></center>
 <center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>🐮 Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/>  <?=$e->getMessage();?></center>
 <?php
 <?php
 }
 }
+
 $_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
 $_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
 setcookie('language', $DEFAULT_LANG);
 setcookie('language', $DEFAULT_LANG);
 if (isset($_COOKIE['language'])) {
 if (isset($_COOKIE['language'])) {
@@ -82,4 +93,4 @@ require_once 'lang/lang.en.php';
 include 'lang/lang.'.$_SESSION['mailcow_locale'].'.php';
 include 'lang/lang.'.$_SESSION['mailcow_locale'].'.php';
 require_once 'inc/functions.inc.php';
 require_once 'inc/functions.inc.php';
 require_once 'inc/triggers.inc.php';
 require_once 'inc/triggers.inc.php';
-(!isset($_SESSION['mailcow_cc_username'])) ? init_db_schema() : null;
+(!isset($_SESSION['mailcow_cc_username'])) ? init_db_schema() : null;

+ 117 - 0
data/web/inc/tfa_modals.php

@@ -0,0 +1,117 @@
+<div class="modal fade" id="YubiOTPModal" tabindex="-1" role="dialog" aria-labelledby="YubiOTPModalLabel">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header"><b><?=$lang['tfa']['yubi_otp'];?></b></div>
+      <div class="modal-body">
+      <form role="form" method="post">
+        <div class="form-group">
+          <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+        </div>
+        <div class="form-group">
+          <div class="input-group">
+            <span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
+            <input type="text" name="otp_token" class="form-control" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
+            <input type="hidden" name="tfa_method" value="yubi_otp">
+          </div>
+        </div>
+        <button class="btn btn-sm btn-default" type="submit" name="set_tfa"><?=$lang['user']['save_changes'];?></button>
+      </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="modal fade" id="U2FModal" tabindex="-1" role="dialog" aria-labelledby="U2FModalLabel">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header"><b><?=$lang['tfa']['u2f'];?></b></div>
+      <div class="modal-body">
+        <form role="form" method="post" id="u2f_reg_form">
+          <div class="form-group">
+            <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+          </div>
+          <p>Please enter your current password and use your U2F USB stick to confirm your identity.</p>
+          <div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
+          <input type="hidden" name="token" id="u2f_register_data"/>
+          <input type="hidden" name="tfa_method" value="u2f">
+          <input type="hidden" name="set_tfa"/><br/>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="modal fade" id="DisableTFAModal" tabindex="-1" role="dialog" aria-labelledby="DisableTFAModalLabel">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header"><b><?=$lang['tfa']['delete_tfa'];?></b></div>
+      <div class="modal-body">
+        <form role="form" method="post">
+          <div class="input-group">
+            <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+            <span class="input-group-btn">
+              <input type="hidden" name="tfa_method" value="none">
+              <button class="btn btn-danger" type="submit" name="set_tfa"><?=$lang['tfa']['delete_tfa'];?></button>
+            </span>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<?php
+if (isset($_SESSION['pending_tfa_method'])):
+  $tfa_method = $_SESSION['pending_tfa_method'];
+?>
+<div class="modal fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header"><button type="button" class="close" data-dismiss="modal">&times;</button><b><?=$lang['tfa'][$tfa_method];?></b></div>
+      <div class="modal-body">
+      <?php
+      switch ($tfa_method) {
+        case "yubi_otp":
+      ?>
+        <form role="form" method="post">
+          <div class="form-group">
+            <div class="input-group">
+              <span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
+              <input type="text" name="token" id="token" class="form-control" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
+              <input type="hidden" name="tfa_method" value="yubi_otp">
+            </div>
+          </div>
+          <button class="btn btn-sm btn-default" type="submit" name="verify_tfa_login"><?=$lang['login']['login'];?></button>
+        </form>
+      <?php
+        break;
+        case "u2f":
+      ?>
+        <form role="form" method="post" id="u2f_auth_form">
+          <p>Please use your U2F USB stick to confirm your identity now.</p>
+          <div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
+          <input type="hidden" name="token" id="u2f_auth_data"/>
+          <input type="hidden" name="tfa_method" value="u2f">
+          <input type="hidden" name="verify_tfa_login"/><br/>
+        </form>
+      <?php
+        break;
+        case "totp":
+      ?>
+       <div class="empty"></div>
+      <?php
+        break;
+        case "hotp":
+      ?>
+       <div class="empty"></div>
+      <?php
+        break;
+      }
+      ?>
+      </div>
+    </div>
+  </div>
+</div>
+<?php
+endif;
+?>

+ 69 - 47
data/web/inc/triggers.inc.php

@@ -1,4 +1,15 @@
 <?php
 <?php
+if (isset($_POST["verify_tfa_login"])) {
+  if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST["token"])) {
+    $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
+    $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
+    unset($_SESSION['pending_mailcow_cc_username']);
+    unset($_SESSION['pending_mailcow_cc_role']);
+    unset($_SESSION['pending_tfa_method']);
+		header("Location: /user.php");
+  }
+}
+
 if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 	$login_user = strtolower(trim($_POST["login_user"]));
 	$login_user = strtolower(trim($_POST["login_user"]));
 	$as = check_login($login_user, $_POST["pass_user"]);
 	$as = check_login($login_user, $_POST["pass_user"]);
@@ -17,13 +28,19 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 		$_SESSION['mailcow_cc_role'] = "user";
 		$_SESSION['mailcow_cc_role'] = "user";
 		header("Location: /user.php");
 		header("Location: /user.php");
 	}
 	}
-	else {
+	elseif ($as != "pending") {
+    unset($_SESSION['pending_mailcow_cc_username']);
+    unset($_SESSION['pending_mailcow_cc_role']);
+    unset($_SESSION['pending_tfa_method']);
+		unset($_SESSION['mailcow_cc_username']);
+		unset($_SESSION['mailcow_cc_role']);
 		$_SESSION['return'] = array(
 		$_SESSION['return'] = array(
 			'type' => 'danger',
 			'type' => 'danger',
 			'msg' => $lang['danger']['login_failed']
 			'msg' => $lang['danger']['login_failed']
 		);
 		);
 	}
 	}
 }
 }
+
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
 	if (isset($_GET["duallogin"])) {
 	if (isset($_GET["duallogin"])) {
     if (filter_var($_GET["duallogin"], FILTER_VALIDATE_EMAIL)) {
     if (filter_var($_GET["duallogin"], FILTER_VALIDATE_EMAIL)) {
@@ -39,9 +56,9 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
       }
       }
     }
     }
   }
   }
-  
-	if (isset($_POST["set_admin_account"])) {
-		set_admin_account($_POST);
+
+	if (isset($_POST["edit_admin_account"])) {
+		edit_admin_account($_POST);
 	}
 	}
 	if (isset($_POST["dkim_delete_key"])) {
 	if (isset($_POST["dkim_delete_key"])) {
 		dkim_delete_key($_POST);
 		dkim_delete_key($_POST);
@@ -55,9 +72,6 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
 	if (isset($_POST["delete_domain_admin"])) {
 	if (isset($_POST["delete_domain_admin"])) {
 		delete_domain_admin($_POST);
 		delete_domain_admin($_POST);
 	}
 	}
-	if (isset($_POST["edit_domain_admin"])) {
-		edit_domain_admin($_POST);
-	}
 }
 }
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") {
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") {
 	if (isset($_POST["edit_user_account"])) {
 	if (isset($_POST["edit_user_account"])) {
@@ -87,56 +101,64 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user
 	if (isset($_POST["delete_syncjob"])) {
 	if (isset($_POST["delete_syncjob"])) {
 		delete_syncjob($_POST);
 		delete_syncjob($_POST);
 	}
 	}
-	if (isset($_POST["trigger_set_time_limited_aliases"])) {
+	if (isset($_POST["set_time_limited_aliases"])) {
 		set_time_limited_aliases($_POST);
 		set_time_limited_aliases($_POST);
 	}
 	}
 }
 }
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
+	if (isset($_POST["edit_domain_admin"])) {
+		edit_domain_admin($_POST);
+	}
+	if (isset($_POST["set_tfa"])) {
+		set_tfa($_POST);
+	}
 	if (isset($_POST["trigger_add_policy_list_item"])) {
 	if (isset($_POST["trigger_add_policy_list_item"])) {
 		add_policy_list_item($_POST);
 		add_policy_list_item($_POST);
 	}
 	}
 	if (isset($_POST["trigger_delete_policy_list_item"])) {
 	if (isset($_POST["trigger_delete_policy_list_item"])) {
 		delete_policy_list_item($_POST);
 		delete_policy_list_item($_POST);
 	}
 	}
-	if (isset($_POST["trigger_mailbox_action"])) {
-		switch ($_POST["trigger_mailbox_action"]) {
-			case "adddomain":
-				mailbox_add_domain($_POST);
-			break;
-			case "addalias":
-				mailbox_add_alias($_POST);
-			break;
-			case "editalias":
-				mailbox_edit_alias($_POST);
-			break;
-			case "addaliasdomain":
-				mailbox_add_alias_domain($_POST);
-			break;
-			case "addmailbox":
-				mailbox_add_mailbox($_POST);
-			break;
-			case "editdomain":
-				mailbox_edit_domain($_POST);
-			break;
-			case "editmailbox":
-				mailbox_edit_mailbox($_POST);
-			break;
-			case "deletedomain":
-				mailbox_delete_domain($_POST);
-			break;
-			case "deletealias":
-				mailbox_delete_alias($_POST);
-			break;
-			case "deletealiasdomain":
-				mailbox_delete_alias_domain($_POST);
-			break;
-			case "editaliasdomain":
-				mailbox_edit_alias_domain($_POST);
-			break;
-			case "deletemailbox":
-				mailbox_delete_mailbox($_POST);
-			break;
-		}
+	if (isset($_POST["mailbox_add_domain"])) {
+		mailbox_add_domain($_POST);
+	}
+	if (isset($_POST["mailbox_add_alias"])) {
+		mailbox_add_alias($_POST);
+	}
+	if (isset($_POST["mailbox_add_alias_domain"])) {
+		mailbox_add_alias_domain($_POST);
+	}
+	if (isset($_POST["mailbox_add_mailbox"])) {
+		mailbox_add_mailbox($_POST);
+	}
+	if (isset($_POST["mailbox_add_mailbox"])) {
+		mailbox_add_mailbox($_POST);
+	}
+	if (isset($_POST["mailbox_edit_alias"])) {
+		mailbox_edit_alias($_POST);
+	}
+	if (isset($_POST["mailbox_edit_domain"])) {
+		mailbox_edit_domain($_POST);
+	}
+	if (isset($_POST["mailbox_edit_mailbox"])) {
+		mailbox_edit_mailbox($_POST);
+	}
+	if (isset($_POST["mailbox_edit_alias_domain"])) {
+		mailbox_edit_alias_domain($_POST);
+	}
+	if (isset($_POST["trigger_delete_policy_list_item"])) {
+		delete_policy_list_item($_POST);
+	}
+	if (isset($_POST["mailbox_delete_domain"])) {
+		mailbox_delete_domain($_POST);
+	}
+	if (isset($_POST["mailbox_delete_alias"])) {
+		mailbox_delete_alias($_POST);
+	}
+	if (isset($_POST["mailbox_delete_alias_domain"])) {
+		mailbox_delete_alias_domain($_POST);
+	}
+	if (isset($_POST["mailbox_delete_mailbox"])) {
+		mailbox_delete_mailbox($_POST);
 	}
 	}
 }
 }
 ?>
 ?>

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

@@ -1,5 +1,6 @@
 <?php
 <?php
-error_reporting(E_ERROR | E_WARNING);
+// error_reporting(E_ERROR | E_WARNING);
+error_reporting(E_ALL);
 
 
 /*
 /*
 PLEASE USE THE FILE "vars.local.inc.php" TO OVERWRITE SETTINGS AND MAKE THEM PERSISTENT!
 PLEASE USE THE FILE "vars.local.inc.php" TO OVERWRITE SETTINGS AND MAKE THEM PERSISTENT!
@@ -33,4 +34,9 @@ $DEFAULT_LANG = "en";
 // simplex, slate, spacelab, superhero, united, yeti
 // simplex, slate, spacelab, superhero, united, yeti
 // See https://bootswatch.com/
 // See https://bootswatch.com/
 $DEFAULT_THEME = "lumen";
 $DEFAULT_THEME = "lumen";
+
+// If you want to use Yubico TFA methods, setup an ID and a key here: https://upgrade.yubico.com/getapikey/
+// Remember to override this value using vars.local.inc.php, do not change it here.
+$YUBI_API['ID'] = "";
+$YUBI_API['KEY'] = "";
 ?>
 ?>

+ 651 - 0
data/web/js/u2f-api.js

@@ -0,0 +1,651 @@
+// Copyright 2014-2015 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+/**
+ * @fileoverview The U2F api.
+ */
+
+'use strict';
+
+/** Namespace for the U2F api.
+ * @type {Object}
+ */
+var u2f = u2f || {};
+
+/**
+ * The U2F extension id
+ * @type {string}
+ * @const
+ */
+u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+
+/**
+ * Message types for messsages to/from the extension
+ * @const
+ * @enum {string}
+ */
+u2f.MessageTypes = {
+    'U2F_REGISTER_REQUEST': 'u2f_register_request',
+    'U2F_SIGN_REQUEST': 'u2f_sign_request',
+    'U2F_REGISTER_RESPONSE': 'u2f_register_response',
+    'U2F_SIGN_RESPONSE': 'u2f_sign_response'
+};
+
+/**
+ * Response status codes
+ * @const
+ * @enum {number}
+ */
+u2f.ErrorCodes = {
+    'OK': 0,
+    'OTHER_ERROR': 1,
+    'BAD_REQUEST': 2,
+    'CONFIGURATION_UNSUPPORTED': 3,
+    'DEVICE_INELIGIBLE': 4,
+    'TIMEOUT': 5
+};
+
+/**
+ * A message type for registration requests
+ * @typedef {{
+ *   type: u2f.MessageTypes,
+ *   signRequests: Array<u2f.SignRequest>,
+ *   registerRequests: ?Array<u2f.RegisterRequest>,
+ *   timeoutSeconds: ?number,
+ *   requestId: ?number
+ * }}
+ */
+u2f.Request;
+
+/**
+ * A message for registration responses
+ * @typedef {{
+ *   type: u2f.MessageTypes,
+ *   responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
+ *   requestId: ?number
+ * }}
+ */
+u2f.Response;
+
+/**
+ * An error object for responses
+ * @typedef {{
+ *   errorCode: u2f.ErrorCodes,
+ *   errorMessage: ?string
+ * }}
+ */
+u2f.Error;
+
+/**
+ * Data object for a single sign request.
+ * @typedef {{
+ *   version: string,
+ *   challenge: string,
+ *   keyHandle: string,
+ *   appId: string
+ * }}
+ */
+u2f.SignRequest;
+
+/**
+ * Data object for a sign response.
+ * @typedef {{
+ *   keyHandle: string,
+ *   signatureData: string,
+ *   clientData: string
+ * }}
+ */
+u2f.SignResponse;
+
+/**
+ * Data object for a registration request.
+ * @typedef {{
+ *   version: string,
+ *   challenge: string,
+ *   appId: string
+ * }}
+ */
+u2f.RegisterRequest;
+
+/**
+ * Data object for a registration response.
+ * @typedef {{
+ *   registrationData: string,
+ *   clientData: string
+ * }}
+ */
+u2f.RegisterResponse;
+
+
+// Low level MessagePort API support
+
+/**
+ * Sets up a MessagePort to the U2F extension using the
+ * available mechanisms.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ */
+u2f.getMessagePort = function(callback) {
+    if (typeof chrome != 'undefined' && chrome.runtime) {
+        // The actual message here does not matter, but we need to get a reply
+        // for the callback to run. Thus, send an empty signature request
+        // in order to get a failure response.
+        var msg = {
+            type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+            signRequests: []
+        };
+        chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
+            if (!chrome.runtime.lastError) {
+                // We are on a whitelisted origin and can talk directly
+                // with the extension.
+                u2f.getChromeRuntimePort_(callback);
+            } else {
+                // chrome.runtime was available, but we couldn't message
+                // the extension directly, use iframe
+                u2f.getIframePort_(callback);
+            }
+        });
+    } else if (u2f.isAndroidChrome_()) {
+        u2f.getAuthenticatorPort_(callback);
+    } else {
+        // chrome.runtime was not available at all, which is normal
+        // when this origin doesn't have access to any extensions.
+        u2f.getIframePort_(callback);
+    }
+};
+
+/**
+ * Detect chrome running on android based on the browser's useragent.
+ * @private
+ */
+u2f.isAndroidChrome_ = function() {
+    var userAgent = navigator.userAgent;
+    return userAgent.indexOf('Chrome') != -1 &&
+        userAgent.indexOf('Android') != -1;
+};
+
+/**
+ * Connects directly to the extension via chrome.runtime.connect
+ * @param {function(u2f.WrappedChromeRuntimePort_)} callback
+ * @private
+ */
+u2f.getChromeRuntimePort_ = function(callback) {
+    var port = chrome.runtime.connect(u2f.EXTENSION_ID,
+        {'includeTlsChannelId': true});
+    setTimeout(function() {
+        callback(new u2f.WrappedChromeRuntimePort_(port));
+    }, 0);
+};
+
+/**
+ * Return a 'port' abstraction to the Authenticator app.
+ * @param {function(u2f.WrappedAuthenticatorPort_)} callback
+ * @private
+ */
+u2f.getAuthenticatorPort_ = function(callback) {
+    setTimeout(function() {
+        callback(new u2f.WrappedAuthenticatorPort_());
+    }, 0);
+};
+
+/**
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
+ * @param {Port} port
+ * @constructor
+ * @private
+ */
+u2f.WrappedChromeRuntimePort_ = function(port) {
+    this.port_ = port;
+};
+
+/**
+ * Format a return a sign request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
+    function(signRequests, timeoutSeconds, reqId) {
+        return {
+            type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+            signRequests: signRequests,
+            timeoutSeconds: timeoutSeconds,
+            requestId: reqId
+        };
+    };
+
+/**
+ * Format a return a register request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
+    function(signRequests, registerRequests, timeoutSeconds, reqId) {
+        return {
+            type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+            signRequests: signRequests,
+            registerRequests: registerRequests,
+            timeoutSeconds: timeoutSeconds,
+            requestId: reqId
+        };
+    };
+
+/**
+ * Posts a message on the underlying channel.
+ * @param {Object} message
+ */
+u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
+    this.port_.postMessage(message);
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface. Works only for the
+ * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
+    function(eventName, handler) {
+        var name = eventName.toLowerCase();
+        if (name == 'message' || name == 'onmessage') {
+            this.port_.onMessage.addListener(function(message) {
+                // Emulate a minimal MessageEvent object
+                handler({'data': message});
+            });
+        } else {
+            console.error('WrappedChromeRuntimePort only supports onMessage');
+        }
+    };
+
+/**
+ * Wrap the Authenticator app with a MessagePort interface.
+ * @constructor
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_ = function() {
+    this.requestId_ = -1;
+    this.requestObject_ = null;
+}
+
+/**
+ * Launch the Authenticator intent.
+ * @param {Object} message
+ */
+u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
+    var intentLocation = /** @type {string} */ (message);
+    document.location = intentLocation;
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
+    function(eventName, handler) {
+        var name = eventName.toLowerCase();
+        if (name == 'message') {
+            var self = this;
+            /* Register a callback to that executes when
+             * chrome injects the response. */
+            window.addEventListener(
+                'message', self.onRequestUpdate_.bind(self, handler), false);
+        } else {
+            console.error('WrappedAuthenticatorPort only supports message');
+        }
+    };
+
+/**
+ * Callback invoked  when a response is received from the Authenticator.
+ * @param function({data: Object}) callback
+ * @param {Object} message message Object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
+    function(callback, message) {
+        var messageObject = JSON.parse(message.data);
+        var intentUrl = messageObject['intentURL'];
+
+        var errorCode = messageObject['errorCode'];
+        var responseObject = null;
+        if (messageObject.hasOwnProperty('data')) {
+            responseObject = /** @type {Object} */ (
+                JSON.parse(messageObject['data']));
+            responseObject['requestId'] = this.requestId_;
+        }
+
+        /* Sign responses from the authenticator do not conform to U2F,
+         * convert to U2F here. */
+        responseObject = this.doResponseFixups_(responseObject);
+        callback({'data': responseObject});
+    };
+
+/**
+ * Fixup the response provided by the Authenticator to conform with
+ * the U2F spec.
+ * @param {Object} responseData
+ * @return {Object} the U2F compliant response object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
+    function(responseObject) {
+        if (responseObject.hasOwnProperty('responseData')) {
+            return responseObject;
+        } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
+            // Only sign responses require fixups.  If this is not a response
+            // to a sign request, then an internal error has occurred.
+            return {
+                'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
+                'responseData': {
+                    'errorCode': u2f.ErrorCodes.OTHER_ERROR,
+                    'errorMessage': 'Internal error: invalid response from Authenticator'
+                }
+            };
+        }
+
+        /* Non-conformant sign response, do fixups. */
+        var encodedChallengeObject = responseObject['challenge'];
+        if (typeof encodedChallengeObject !== 'undefined') {
+            var challengeObject = JSON.parse(atob(encodedChallengeObject));
+            var serverChallenge = challengeObject['challenge'];
+            var challengesList = this.requestObject_['signData'];
+            var requestChallengeObject = null;
+            for (var i = 0; i < challengesList.length; i++) {
+                var challengeObject = challengesList[i];
+                if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
+                    requestChallengeObject = challengeObject;
+                    break;
+                }
+            }
+        }
+        var responseData = {
+            'errorCode': responseObject['resultCode'],
+            'keyHandle': responseObject['keyHandle'],
+            'signatureData': responseObject['signature'],
+            'clientData': encodedChallengeObject
+        };
+        return {
+            'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
+            'responseData': responseData,
+            'requestId': responseObject['requestId']
+        }
+    };
+
+/**
+ * Base URL for intents to Authenticator.
+ * @const
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
+    'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
+
+/**
+ * Format a return a sign request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {string}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
+    function(signRequests, timeoutSeconds, reqId) {
+        if (!signRequests || signRequests.length == 0) {
+            return null;
+        }
+        /* TODO(fixme): stash away requestId, as the authenticator app does
+         * not return it for sign responses. */
+        this.requestId_ = reqId;
+        /* TODO(fixme): stash away the signRequests, to deal with the legacy
+         * response format returned by the Authenticator app. */
+        this.requestObject_ = {
+            'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
+            'signData': signRequests,
+            'requestId': reqId,
+            'timeout': timeoutSeconds
+        };
+
+        var appId = signRequests[0]['appId'];
+        var intentUrl =
+            u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+            ';S.appId=' + encodeURIComponent(appId) +
+            ';S.eventId=' + reqId +
+            ';S.challenges=' +
+            encodeURIComponent(
+                JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
+        return intentUrl;
+    };
+
+/**
+ * Get the browser data objects from the challenge list
+ * @param {Array} challenges list of challenges
+ * @return {Array} list of browser data objects
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_
+    .prototype.getBrowserDataList_ = function(challenges) {
+    return challenges
+        .map(function(challenge) {
+            var browserData = {
+                'typ': 'navigator.id.getAssertion',
+                'challenge': challenge['challenge']
+            };
+            var challengeObject = {
+                'challenge' : browserData,
+                'keyHandle' : challenge['keyHandle']
+            };
+            return challengeObject;
+        });
+};
+
+/**
+ * Format a return a register request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} enrollChallenges
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
+    function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
+        if (!enrollChallenges || enrollChallenges.length == 0) {
+            return null;
+        }
+        // Assume the appId is the same for all enroll challenges.
+        var appId = enrollChallenges[0]['appId'];
+        var registerRequests = [];
+        for (var i = 0; i < enrollChallenges.length; i++) {
+            var registerRequest = {
+                'challenge': enrollChallenges[i]['challenge'],
+                'version': enrollChallenges[i]['version']
+            };
+            if (enrollChallenges[i]['appId'] != appId) {
+                // Only include the appId when it differs from the first appId.
+                registerRequest['appId'] = enrollChallenges[i]['appId'];
+            }
+            registerRequests.push(registerRequest);
+        }
+        var registeredKeys = [];
+        if (signRequests) {
+            for (i = 0; i < signRequests.length; i++) {
+                var key = {
+                    'keyHandle': signRequests[i]['keyHandle'],
+                    'version': signRequests[i]['version']
+                };
+                // Only include the appId when it differs from the appId that's
+                // being registered now.
+                if (signRequests[i]['appId'] != appId) {
+                    key['appId'] = signRequests[i]['appId'];
+                }
+                registeredKeys.push(key);
+            }
+        }
+        var request = {
+            'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
+            'appId': appId,
+            'registerRequests': registerRequests,
+            'registeredKeys': registeredKeys,
+            'requestId': reqId,
+            'timeoutSeconds': timeoutSeconds
+        };
+        var intentUrl =
+            u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+            ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
+            ';end';
+        /* TODO(fixme): stash away requestId, this is is not necessary for
+         * register requests, but here to keep parity with sign.
+         */
+        this.requestId_ = reqId;
+        return intentUrl;
+    };
+
+
+/**
+ * Sets up an embedded trampoline iframe, sourced from the extension.
+ * @param {function(MessagePort)} callback
+ * @private
+ */
+u2f.getIframePort_ = function(callback) {
+    // Create the iframe
+    var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
+    var iframe = document.createElement('iframe');
+    iframe.src = iframeOrigin + '/u2f-comms.html';
+    iframe.setAttribute('style', 'display:none');
+    document.body.appendChild(iframe);
+
+    var channel = new MessageChannel();
+    var ready = function(message) {
+        if (message.data == 'ready') {
+            channel.port1.removeEventListener('message', ready);
+            callback(channel.port1);
+        } else {
+            console.error('First event on iframe port was not "ready"');
+        }
+    };
+    channel.port1.addEventListener('message', ready);
+    channel.port1.start();
+
+    iframe.addEventListener('load', function() {
+        // Deliver the port to the iframe and initialize
+        iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
+    });
+};
+
+
+// High-level JS API
+
+/**
+ * Default extension response timeout in seconds.
+ * @const
+ */
+u2f.EXTENSION_TIMEOUT_SEC = 30;
+
+/**
+ * A singleton instance for a MessagePort to the extension.
+ * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
+ * @private
+ */
+u2f.port_ = null;
+
+/**
+ * Callbacks waiting for a port
+ * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
+ * @private
+ */
+u2f.waitingForPort_ = [];
+
+/**
+ * A counter for requestIds.
+ * @type {number}
+ * @private
+ */
+u2f.reqCounter_ = 0;
+
+/**
+ * A map from requestIds to client callbacks
+ * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
+ *                       |function((u2f.Error|u2f.SignResponse)))>}
+ * @private
+ */
+u2f.callbackMap_ = {};
+
+/**
+ * Creates or retrieves the MessagePort singleton to use.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ * @private
+ */
+u2f.getPortSingleton_ = function(callback) {
+    if (u2f.port_) {
+        callback(u2f.port_);
+    } else {
+        if (u2f.waitingForPort_.length == 0) {
+            u2f.getMessagePort(function(port) {
+                u2f.port_ = port;
+                u2f.port_.addEventListener('message',
+                    /** @type {function(Event)} */ (u2f.responseHandler_));
+
+                // Careful, here be async callbacks. Maybe.
+                while (u2f.waitingForPort_.length)
+                    u2f.waitingForPort_.shift()(u2f.port_);
+            });
+        }
+        u2f.waitingForPort_.push(callback);
+    }
+};
+
+/**
+ * Handles response messages from the extension.
+ * @param {MessageEvent.<u2f.Response>} message
+ * @private
+ */
+u2f.responseHandler_ = function(message) {
+    var response = message.data;
+    var reqId = response['requestId'];
+    if (!reqId || !u2f.callbackMap_[reqId]) {
+        console.error('Unknown or missing requestId in response.');
+        return;
+    }
+    var cb = u2f.callbackMap_[reqId];
+    delete u2f.callbackMap_[reqId];
+    cb(response['responseData']);
+};
+
+/**
+ * Dispatches an array of sign requests to available U2F tokens.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
+    u2f.getPortSingleton_(function(port) {
+        var reqId = ++u2f.reqCounter_;
+        u2f.callbackMap_[reqId] = callback;
+        var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+            opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+        var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
+        port.postMessage(req);
+    });
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * @param {Array<u2f.RegisterRequest>} registerRequests
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.register = function(registerRequests, signRequests,
+                        callback, opt_timeoutSeconds) {
+    u2f.getPortSingleton_(function(port) {
+        var reqId = ++u2f.reqCounter_;
+        u2f.callbackMap_[reqId] = callback;
+        var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+            opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+        var req = port.formatRegisterRequest_(
+            signRequests, registerRequests, timeoutSeconds, reqId);
+        port.postMessage(req);
+    });
+};

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

@@ -15,9 +15,9 @@ $(document).ready(function() {
 		}
 		}
 	});
 	});
 	// Show generate button after time selection
 	// Show generate button after time selection
-	$('#trigger_set_time_limited_aliases').hide(); 
+	$('#generate_tla').hide(); 
 	$('#validity').change(function(){
 	$('#validity').change(function(){
-		$('#trigger_set_time_limited_aliases').show(); 
+		$('#generate_tla').show(); 
 	});
 	});
 
 
 	// Init Bootstrap Switch
 	// Init Bootstrap Switch

+ 27 - 2
data/web/json_api.php

@@ -1,7 +1,7 @@
 <?php
 <?php
 require_once 'inc/prerequisites.inc.php';
 require_once 'inc/prerequisites.inc.php';
-error_reporting(0);
-if (isset($_SESSION['mailcow_cc_role'])) {
+error_reporting(E_ALL);
+if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
   if ($_GET['action'] && $_GET['object']) {
   if ($_GET['action'] && $_GET['object']) {
     $action = $_GET['action'];
     $action = $_GET['action'];
     $object = $_GET['object'];
     $object = $_GET['object'];
@@ -24,6 +24,31 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
           echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
         }
         }
         break;
         break;
+      case "get_u2f_reg_challenge":
+        if (
+          ($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin")
+          &&
+          ($_SESSION["mailcow_cc_username"] == $object)
+        ) {
+          $data = $u2f->getRegisterData(get_u2f_registrations($object));
+          list($req, $sigs) = $data;
+          $_SESSION['regReq'] = json_encode($req);
+          echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';';
+        }
+        else {
+          echo '{}';
+        }
+        break;
+      case "get_u2f_auth_challenge":
+        if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
+          $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object)));
+          $_SESSION['authReq']  = $reqs;
+          echo 'var req = ' . $reqs . ';';
+        }
+        else {
+          echo '{}';
+        }
+        break;
       default:
       default:
         echo '{}';
         echo '{}';
         break;
         break;

+ 16 - 8
data/web/lang/lang.de.php

@@ -94,10 +94,10 @@ $lang['user']['user_settings'] = 'Benutzereinstellungen';
 $lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen';
 $lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen';
 $lang['user']['mailbox_details'] = 'Mailbox-Details';
 $lang['user']['mailbox_details'] = 'Mailbox-Details';
 $lang['user']['change_password'] = 'Passwort ändern';
 $lang['user']['change_password'] = 'Passwort ändern';
-$lang['user']['new_password'] = 'Neues Passwort:';
+$lang['user']['new_password'] = 'Neues Passwort';
 $lang['user']['save_changes'] = 'Änderungen speichern';
 $lang['user']['save_changes'] = 'Änderungen speichern';
-$lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen):';
-$lang['user']['new_password_repeat'] = 'Neues Passwort (Wiederholung):';
+$lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen)';
+$lang['user']['new_password_repeat'] = 'Neues Passwort (Wiederholung)';
 $lang['user']['new_password_description'] = 'Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.';
 $lang['user']['new_password_description'] = 'Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.';
 $lang['user']['did_you_know'] = '<b>Wussten Sie schon?</b> Sie können Ihre E-Mail-Adresse mit Tags versehen, etwa "ich+<b>Privat</b>@example.com", um Nachrichten automatisch in einem Unterordner (Beispiel: "Privat") abzulegen.';
 $lang['user']['did_you_know'] = '<b>Wussten Sie schon?</b> Sie können Ihre E-Mail-Adresse mit Tags versehen, etwa "ich+<b>Privat</b>@example.com", um Nachrichten automatisch in einem Unterordner (Beispiel: "Privat") abzulegen.';
 $lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse';
 $lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse';
@@ -351,11 +351,19 @@ $lang['login']['login'] = 'Anmelden';
 $lang['login']['previous'] = 'Vorherige Seite';
 $lang['login']['previous'] = 'Vorherige Seite';
 $lang['login']['delayed'] = 'Login wurde zur Sicherheit um %s Sekunde/n verzögert.';
 $lang['login']['delayed'] = 'Login wurde zur Sicherheit um %s Sekunde/n verzögert.';
 
 
-$lang['login']['tfa'] = 'Zwei-Faktor-Authentifizierung';
-$lang['login']['tfa_details'] = 'Bitte bestätigen Sie Ihr Einmalpasswort im folgenden Feld';
-$lang['login']['confirm'] = 'Bestätigen';
-$lang['login']['otp'] = 'Einmalpasswort';
-$lang['login']['trash_login'] = 'Login verwerfen';
+$lang['tfa']['tfa'] = "Two-Factor Authentication";
+$lang['tfa']['set_tfa'] = "Konfiguriere Two-Factor Authentication Methode";
+$lang['tfa']['yubi_otp'] = "Yubico OTP Authentifizierung";
+$lang['tfa']['u2f'] = "U2F Authentifizierung";
+$lang['tfa']['hotp'] = "HOTP Authentifizierung";
+$lang['tfa']['totp'] = "TOTP Authentifizierung";
+$lang['tfa']['none'] = "Deaktiviert";
+$lang['tfa']['delete_tfa'] = "Deaktiviere TFA";
+$lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field";
+$lang['tfa']['confirm'] = "Bestätigen";
+$lang['tfa']['otp'] = "Einmalpasswort";
+$lang['tfa']['trash_login'] = "Login verwerfen";
+$lang['tfa']['select'] = "Bitte auswählen";
 
 
 $lang['admin']['search_domain_da'] = 'Domains durchsuchen';
 $lang['admin']['search_domain_da'] = 'Domains durchsuchen';
 $lang['admin']['restrictions'] = 'Postifx Restriktionen';
 $lang['admin']['restrictions'] = 'Postifx Restriktionen';

+ 18 - 9
data/web/lang/lang.en.php

@@ -89,6 +89,7 @@ $lang['warning']['spam_alias_temp_error'] = "Temporary error: Cannot add spam al
 $lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded";
 $lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded";
 $lang['danger']['fetchmail_active'] = "A process is already running, please wait for it to finish.";
 $lang['danger']['fetchmail_active'] = "A process is already running, please wait for it to finish.";
 $lang['danger']['validity_missing'] = 'Please assign a period of validity';
 $lang['danger']['validity_missing'] = 'Please assign a period of validity';
+$lang['danger']['tfa_token_invalid'] = 'TFA token is invalid';
 $lang['user']['on'] = "On";
 $lang['user']['on'] = "On";
 $lang['user']['off'] = "Off";
 $lang['user']['off'] = "Off";
 $lang['user']['user_change_fn'] = "";
 $lang['user']['user_change_fn'] = "";
@@ -96,10 +97,10 @@ $lang['user']['user_settings'] = 'User settings';
 $lang['user']['mailbox_settings'] = 'Mailbox settings';
 $lang['user']['mailbox_settings'] = 'Mailbox settings';
 $lang['user']['mailbox_details'] = 'Mailbox details';
 $lang['user']['mailbox_details'] = 'Mailbox details';
 $lang['user']['change_password'] = 'Change password';
 $lang['user']['change_password'] = 'Change password';
-$lang['user']['new_password'] = 'New password:';
+$lang['user']['new_password'] = 'New password';
 $lang['user']['save_changes'] = 'Save changes';
 $lang['user']['save_changes'] = 'Save changes';
-$lang['user']['password_now'] = 'Current password (confirm changes):';
-$lang['user']['new_password_repeat'] = 'Confirmation password (repeat):';
+$lang['user']['password_now'] = 'Current password (confirm changes)';
+$lang['user']['new_password_repeat'] = 'Confirmation password (repeat)';
 $lang['user']['new_password_description'] = 'Requirement: 6 characters long, letters and numbers.';
 $lang['user']['new_password_description'] = 'Requirement: 6 characters long, letters and numbers.';
 $lang['user']['did_you_know'] = '<b>Did you know?</b> You can use tags in your email address ("me+<b>privat</b>@example.com") to move messages to a folder automatically (example: "privat").';
 $lang['user']['did_you_know'] = '<b>Did you know?</b> You can use tags in your email address ("me+<b>privat</b>@example.com") to move messages to a folder automatically (example: "privat").';
 $lang['user']['spam_aliases'] = 'Temporary email aliases';
 $lang['user']['spam_aliases'] = 'Temporary email aliases';
@@ -252,7 +253,7 @@ $lang['delete']['previous'] = 'Previous page';
 
 
 $lang['edit']['syncjob'] = 'Edit sync job';
 $lang['edit']['syncjob'] = 'Edit sync job';
 $lang['edit']['save'] = 'Save changes';
 $lang['edit']['save'] = 'Save changes';
-$lang['edit']['username'] = 'Save changes';
+$lang['edit']['username'] = 'Username';
 $lang['edit']['hostname'] = 'Hostname';
 $lang['edit']['hostname'] = 'Hostname';
 $lang['edit']['encryption'] = 'Encryption';
 $lang['edit']['encryption'] = 'Encryption';
 $lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote<br /><small>(0 = ignore age)</small>';
 $lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote<br /><small>(0 = ignore age)</small>';
@@ -354,11 +355,19 @@ $lang['login']['login'] = 'Login';
 $lang['login']['previous'] = "Previous page";
 $lang['login']['previous'] = "Previous page";
 $lang['login']['delayed'] = 'Login was delayed by %s seconds.';
 $lang['login']['delayed'] = 'Login was delayed by %s seconds.';
 
 
-$lang['login']['tfa'] = "Two-factor authentication";
-$lang['login']['tfa_details'] = "Please confirm your one-time password in the below field";
-$lang['login']['confirm'] = "Confirm";
-$lang['login']['otp'] = "One-time password";
-$lang['login']['trash_login'] = "Trash login";
+$lang['tfa']['tfa'] = "Two-factor authentication";
+$lang['tfa']['set_tfa'] = "Set two-factor authentication method";
+$lang['tfa']['yubi_otp'] = "Yubico OTP authentication";
+$lang['tfa']['u2f'] = "U2F authentication";
+$lang['tfa']['hotp'] = "HOTP authentication";
+$lang['tfa']['totp'] = "TOTP authentication";
+$lang['tfa']['none'] = "Deaktiviert";
+$lang['tfa']['delete_tfa'] = "Disable TFA";
+$lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field";
+$lang['tfa']['confirm'] = "Confirm";
+$lang['tfa']['otp'] = "One-time password";
+$lang['tfa']['trash_login'] = "Trash login";
+$lang['tfa']['select'] = "Please select";
 
 
 $lang['admin']['search_domain_da'] = 'Search domains';
 $lang['admin']['search_domain_da'] = 'Search domains';
 $lang['admin']['restrictions'] = 'Postifx Restrictions';
 $lang['admin']['restrictions'] = 'Postifx Restrictions';

+ 3 - 3
data/web/lang/lang.nl.php

@@ -93,10 +93,10 @@ $lang['user']['user_settings'] = 'Gebruikersinstellingen';
 $lang['user']['mailbox_settings'] = 'Postvakinstellingen';
 $lang['user']['mailbox_settings'] = 'Postvakinstellingen';
 $lang['user']['mailbox_details'] = 'Postvakdetails';
 $lang['user']['mailbox_details'] = 'Postvakdetails';
 $lang['user']['change_password'] = 'Verander wachtwoord';
 $lang['user']['change_password'] = 'Verander wachtwoord';
-$lang['user']['new_password'] = 'Nieuw wachtwoord:';
+$lang['user']['new_password'] = 'Nieuw wachtwoord';
 $lang['user']['save_changes'] = 'Wijzigingen opslaan';
 $lang['user']['save_changes'] = 'Wijzigingen opslaan';
-$lang['user']['password_now'] = 'Huidig wachtwoord (bevestig wijzigingen):';
-$lang['user']['new_password_repeat'] = 'Bevestig wachtwoord (herhalen):';
+$lang['user']['password_now'] = 'Huidig wachtwoord (bevestig wijzigingen)';
+$lang['user']['new_password_repeat'] = 'Bevestig wachtwoord (herhalen)';
 $lang['user']['new_password_description'] = 'Vereisten: 6 karakters lang, letters en nummers.';
 $lang['user']['new_password_description'] = 'Vereisten: 6 karakters lang, letters en nummers.';
 $lang['user']['did_you_know'] = '<b>Wist u dat?</b> U kunt tags in het e-mailadres gebruiken ("me+<b>prive</b>@voorbeeld.nl") om berichten automatisch naar een bijbehorende map te sturen (voorbeeld: "prive").';
 $lang['user']['did_you_know'] = '<b>Wist u dat?</b> U kunt tags in het e-mailadres gebruiken ("me+<b>prive</b>@voorbeeld.nl") om berichten automatisch naar een bijbehorende map te sturen (voorbeeld: "prive").';
 $lang['user']['spam_aliases'] = 'Tijdelijk e-mailadres';
 $lang['user']['spam_aliases'] = 'Tijdelijk e-mailadres';

+ 3 - 3
data/web/lang/lang.pt.php

@@ -91,10 +91,10 @@ $lang['user']['user_settings'] = 'Configurações do usuário';
 $lang['user']['mailbox_settings'] = 'Configrações da conta';
 $lang['user']['mailbox_settings'] = 'Configrações da conta';
 $lang['user']['mailbox_details'] = 'Detalhes da conta';
 $lang['user']['mailbox_details'] = 'Detalhes da conta';
 $lang['user']['change_password'] = 'Alterar senha';
 $lang['user']['change_password'] = 'Alterar senha';
-$lang['user']['new_password'] = 'Nova senha:';
+$lang['user']['new_password'] = 'Nova senha';
 $lang['user']['save_changes'] = 'Salvar';
 $lang['user']['save_changes'] = 'Salvar';
-$lang['user']['password_now'] = 'Senha atual (confirme a alteração):';
-$lang['user']['new_password_repeat'] = 'Confirmar senha (repetir):';
+$lang['user']['password_now'] = 'Senha atual (confirme a alteração)';
+$lang['user']['new_password_repeat'] = 'Confirmar senha (repetir)';
 $lang['user']['new_password_description'] = 'Requerido: mínimo de 6 characteres com letras e números.';
 $lang['user']['new_password_description'] = 'Requerido: mínimo de 6 characteres com letras e números.';
 $lang['user']['did_you_know'] = '<b>Você sabia?</b> Você pode usar tags no endereço de email ("conta+<b>privado</b>@example.com") para classificar as mensagens automaticamente para uma determinada pasta (exemplo: "privado").';
 $lang['user']['did_you_know'] = '<b>Você sabia?</b> Você pode usar tags no endereço de email ("conta+<b>privado</b>@example.com") para classificar as mensagens automaticamente para uma determinada pasta (exemplo: "privado").';
 $lang['user']['spam_aliases'] = 'Apelidos temporários';
 $lang['user']['spam_aliases'] = 'Apelidos temporários';

+ 157 - 0
data/web/u2f_api.php

@@ -0,0 +1,157 @@
+<?php
+require_once('inc/prerequisites.inc.php');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
+
+$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://";
+$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']);
+
+function getRegs($username) {
+  global $pdo;
+  $sel = $pdo->prepare("select * from tfa where username = ?");
+  $sel->execute(array($username));
+  return $sel->fetchAll();
+}
+function addReg($username, $reg) {
+  global $pdo;
+  $ins = $pdo->prepare("INSERT INTO `tfa` (`username`, `keyHandle`, `publicKey`, `certificate`, `counter`) values (?, ?, ?, ?, ?)");
+  $ins->execute(array($username, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
+}
+function updateReg($reg) {
+  global $pdo;
+  $upd = $pdo->prepare("update tfa set counter = ? where id = ?");
+  $upd->execute(array($reg->counter, $reg->id));
+}
+?>
+<html>
+<head>
+<script src="js/u2f-api.js"></script>
+<?php
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+  if ((empty($_POST['u2f_username'])) || (!isset($_POST['action']) && !isset($_POST['u2f_register_data']) && !isset($_POST['u2f_auth_data']))) {
+    print_r($_POST);
+    exit();
+  }
+  else {
+    $username = $_POST['u2f_username'];
+    if (isset($_POST['action'])) {
+      switch($_POST['action']) {
+        case 'register':
+          try {
+          $data = $u2f->getRegisterData(getRegs($username));
+          list($req, $sigs) = $data;
+          $_SESSION['regReq'] = json_encode($req);
+?>
+<script>
+var req = <?=json_encode($req);?>;
+var sigs = <?=json_encode($sigs);?>;
+var username = "<?=$username;?>";
+setTimeout(function() {
+  console.log("Register: ", req);
+  u2f.register([req], sigs, function(data) {
+    var form  = document.getElementById('u2f_form');
+    var reg   = document.getElementById('u2f_register_data');
+    var user  = document.getElementById('u2f_username');
+    var status = document.getElementById('u2f_status');
+    console.log("Register callback", data);
+    if (data.errorCode && data.errorCode != 0) {
+      var div = document.getElementById('u2f_return_code');
+      div.innerHTML = 'Error code: ' + data.errorCode;
+      return;
+    }
+    reg.value = JSON.stringify(data);
+    user.value = username;
+    status.value = "1";
+    form.submit();
+  });
+}, 1000);
+</script>
+<?php
+          }
+          catch( Exception $e ) {
+            echo "U2F error: " . $e->getMessage();
+          }
+        break;
+
+        case 'authenticate':
+        try {
+          $reqs = json_encode($u2f->getAuthenticateData(getRegs($username)));
+          $_SESSION['authReq']  = $reqs;
+?>
+<script>
+var req = <?=$reqs;?>;
+var username = "<?=$username;?>";       
+setTimeout(function() {
+  console.log("sign: ", req);
+  u2f.sign(req, function(data) {
+    var form = document.getElementById('u2f_form');
+    var auth = document.getElementById('u2f_auth_data');
+    var user = document.getElementById('u2f_username');
+    console.log("Authenticate callback", data);
+    auth.value = JSON.stringify(data);
+    user.value = username;
+    form.submit();
+  });
+}, 1000);
+</script>
+<?php
+        }
+        catch (Exception $e) {
+          echo "U2F error: " . $e->getMessage();
+        }
+        break;
+      }
+    }
+    if (!empty($_POST['u2f_register_data'])) {
+      try {
+        $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_POST['u2f_register_data']));
+        addReg($username, $reg);
+      }
+      catch (Exception $e) {
+        echo "U2F error: " . $e->getMessage();
+      }
+      finally {
+        echo "Success";
+        $_SESSION['regReq'] = null;
+      }
+    }
+    if (!empty($_POST['u2f_auth_data'])) {
+      try {
+        $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), getRegs($username), json_decode($_POST['u2f_auth_data']));
+        updateReg($reg);
+      }
+      catch (Exception $e) {
+        echo "U2F error: " . $e->getMessage();
+      }
+      finally {
+        echo "Success";
+        $_SESSION['authReq'] = null;
+      }
+    }
+  }
+?>
+</head>
+<body>
+<div id="u2f_return_code"></div>
+<form method="POST" id="u2f_form">
+<input type="hidden" name="u2f_register_data" id="u2f_register_data"/>
+<input type="hidden" name="u2f_auth_data" id="u2f_auth_data"/>
+<input type="hidden" name="u2f_username" id="u2f_username"/><br/>
+<input type="hidden" name="u2f_status" id="u2f_status"/><br/>
+</form>
+<?php
+}
+else {
+?>
+<form method="POST" id="post_form">
+Username: <input name="u2f_username" id="u2f_username"/><br/><hr>
+Action: <br />
+<input value="register" name="action" type="radio"/> Register<br/>
+<input value="authenticate" name="action" type="radio"/> Authenticate<br/>
+<button type="submit">Submit!</button>
+  </form>
+<?php
+}
+?>
+</body>
+</html>

+ 100 - 39
data/web/user.php

@@ -1,53 +1,67 @@
 <?php
 <?php
 require_once("inc/prerequisites.inc.php");
 require_once("inc/prerequisites.inc.php");
-if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+
+  /*
+  / DOMAIN ADMIN
+  */
+
 	require_once("inc/header.inc.php");
 	require_once("inc/header.inc.php");
 	$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 	$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 	$username = $_SESSION['mailcow_cc_username'];
 	$username = $_SESSION['mailcow_cc_username'];
-	$get_tls_policy = get_tls_policy($_SESSION['mailcow_cc_username']);
 ?>
 ?>
 <div class="container">
 <div class="container">
-<h3><?=$lang['user']['mailbox_settings'];?></h3>
-
-<div class="panel panel-default">
-<div class="panel-heading"><?=$lang['user']['mailbox_details'];?></div>
-<div class="panel-body">
-  <form class="form-horizontal" role="form" method="post" autocomplete="off">
-    <div class="form-group">
-      <div class="col-sm-offset-3 col-sm-10">
-        <div class="checkbox">
-          <label><input type="checkbox" name="togglePwNew" id="togglePwNew"> <?=$lang['user']['change_password'];?></label>
-        </div>
+  <h3><?=$lang['user']['user_settings'];?></h3>
+  <div class="panel panel-default">
+  <div class="panel-heading"><?=$lang['user']['user_settings'];?></div>
+  <div class="panel-body">
+    <div class="row">
+      <div class="col-sm-offset-3 col-sm-9">
+        <p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
       </div>
       </div>
     </div>
     </div>
-    <div class="passFields">
-      <div class="form-group">
-        <label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
-        <div class="col-sm-5">
-        <input type="password" class="form-control" name="user_new_pass" id="user_new_pass" autocomplete="off" disabled="disabled" required>
-        </div>
-      </div>
-      <div class="form-group">
-        <label class="control-label col-sm-3" for="user_new_pass2"><?=$lang['user']['new_password_repeat'];?></label>
-        <div class="col-sm-5">
-        <input type="password" class="form-control" name="user_new_pass2" id="user_new_pass2" disabled="disabled" autocomplete="off" required>
-        <p class="help-block"><?=$lang['user']['new_password_description'];?></p>
-        </div>
+    <hr>
+    <div class="row">
+      <div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?></div>
+      <div class="col-md-9 col-xs-7">
+      <p><?=get_tfa()['pretty'];?></p>
       </div>
       </div>
-      <hr>
     </div>
     </div>
-    <div class="form-group">
-      <label class="control-label col-sm-3" for="user_old_pass"><?=$lang['user']['password_now'];?></label>
-      <div class="col-sm-5">
-      <input type="password" class="form-control" name="user_old_pass" id="user_old_pass" autocomplete="off" required>
+    <div class="row">
+      <div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?></div>
+      <div class="col-md-9 col-xs-7">
+        <select id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
+          <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
+          <option value="none"><?=$lang['tfa']['none'];?></option>
+        </select>
       </div>
       </div>
     </div>
     </div>
-    <div class="form-group">
-      <div class="col-sm-offset-3 col-sm-9">
-        <button type="submit" name="edit_user_account" class="btn btn-success btn-default"><?=$lang['user']['save_changes'];?></button>
-      </div>
+  </div>
+</div>
+<?php
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+
+  /*
+  / USER
+  */
+
+	require_once("inc/header.inc.php");
+	$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+	$username = $_SESSION['mailcow_cc_username'];
+	$get_tls_policy = get_tls_policy($_SESSION['mailcow_cc_username']);
+?>
+<div class="container">
+<h3><?=$lang['user']['user_settings'];?></h3>
+
+<div class="panel panel-default">
+<div class="panel-heading"><?=$lang['user']['mailbox_details'];?></div>
+<div class="panel-body">
+  <div class="row">
+    <div class="col-sm-offset-3 col-sm-9">
+      <p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
     </div>
     </div>
-  </form>
+  </div>
   <hr>
   <hr>
   <?php // Get user information about aliases
   <?php // Get user information about aliases
   $user_get_alias_details = user_get_alias_details($username);?>
   $user_get_alias_details = user_get_alias_details($username);?>
@@ -170,7 +184,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
 					<option value="168">1 <?=$lang['user']['week'];?></option>
 					<option value="168">1 <?=$lang['user']['week'];?></option>
 					<option value="672">4 <?=$lang['user']['weeks'];?></option>
 					<option value="672">4 <?=$lang['user']['weeks'];?></option>
 				</select>
 				</select>
-				<button type="submit" name="set_time_limited_aliases" value="generate" class="btn btn-success"><?=$lang['user']['alias_create_random'];?></button>
+				<button type="submit" name="set_time_limited_aliases" id="generate_tla" value="generate" class="btn btn-success"><?=$lang['user']['alias_create_random'];?></button>
 			</div>
 			</div>
 		</div>
 		</div>
 		<div class="form-group">
 		<div class="form-group">
@@ -423,7 +437,16 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>
-<div style="margin-bottom:200px;"></div>
+
+<?php
+}
+if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
+
+  /*
+  / USER OR DOMAIN ADMIN
+  */
+
+?>
 <div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
 <div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
   <div class="modal-dialog" style="width:90%" role="document">
   <div class="modal-dialog" style="width:90%" role="document">
     <div class="modal-content">
     <div class="modal-content">
@@ -433,12 +456,50 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
     </div>
     </div>
   </div>
   </div>
 </div>
 </div>
+
+<div style="margin-bottom:200px;"></div>
+<div class="modal fade" id="pwChangeModal" tabindex="-1" role="dialog" aria-labelledby="pwChangeModalLabel">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-body">
+        <form class="form-horizontal" role="form" method="post" autocomplete="off">
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
+            <div class="col-sm-5">
+            <input type="password" class="form-control" name="user_new_pass" id="user_new_pass" autocomplete="off" required>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="user_new_pass2"><?=$lang['user']['new_password_repeat'];?></label>
+            <div class="col-sm-5">
+            <input type="password" class="form-control" name="user_new_pass2" id="user_new_pass2" autocomplete="off" required>
+            <p class="help-block"><?=$lang['user']['new_password_description'];?></p>
+            </div>
+          </div>
+          <hr>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="user_old_pass"><?=$lang['user']['password_now'];?></label>
+            <div class="col-sm-5">
+            <input type="password" class="form-control" name="user_old_pass" id="user_old_pass" autocomplete="off" required>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <button type="submit" name="edit_<?=($_SESSION['mailcow_cc_role'] == "domainadmin") ? "domain_admin" : "user_account";?>" class="btn btn-sm btn-success"><?=$lang['user']['change_password'];?></button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
 </div> <!-- /container -->
 </div> <!-- /container -->
 <script src="js/sorttable.js"></script>
 <script src="js/sorttable.js"></script>
 <script src="js/user.js"></script>
 <script src="js/user.js"></script>
 <?php
 <?php
 require_once("inc/footer.inc.php");
 require_once("inc/footer.inc.php");
-} else {
+}
+else {
 	header('Location: /');
 	header('Location: /');
 	exit();
 	exit();
 }
 }