瀏覽代碼

Merge pull request #51 from andryyy/dev

Dev to master
André Peters 8 年之前
父節點
當前提交
bdba65686b

+ 4 - 7
data/Dockerfiles/sogo/reconf-domains.sh

@@ -8,10 +8,9 @@ while mysqladmin ping --host mysql --silent; do
 mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
 
 mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, senderacl, home, kind, multiple_bookings) AS
-SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), IFNULL(gs.send_as, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox
+CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, home, kind, multiple_bookings) AS
+SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox
 LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username = mailbox.username
-LEFT OUTER JOIN grouped_sender_acl gs ON gs.username = mailbox.username
 LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
 WHERE mailbox.active = '1';
 EOF
@@ -50,11 +49,10 @@ EOF
 # Generate multi-domain setup
 while read line
 	do
-	DOMAIN_SANE=$(echo ${line} | tr '-' 'b' | tr '.' 'p' | tr -cd '[[:alnum:]]')
 	echo "        <key>${line}</key>
         <dict>
             <key>SOGoMailDomain</key>
-            <string>${DOMAIN_SANE}</string>
+            <string>${line}</string>
             <key>SOGoUserSources</key>
             <array>
                 <dict>
@@ -62,7 +60,6 @@ while read line
                     <array>
                         <string>aliases</string>
                         <string>ad_aliases</string>
-                        <string>senderacl</string>
                     </array>
                     <key>KindFieldName</key>
                     <string>kind</string>
@@ -98,4 +95,4 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
 
 sleep 99999
 
-done;
+done

+ 6 - 6
data/conf/nginx/site.conf

@@ -39,7 +39,7 @@ server {
 
   rewrite ^(/save.+)$ /rspamd$1 last;
   location /rspamd/ {
-    proxy_pass       http://rspamd:11334/;
+    proxy_pass       http://172.22.1.253:11334/;
     proxy_set_header Host      $host;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
@@ -61,7 +61,7 @@ server {
   }
 
   location ^~ /Microsoft-Server-ActiveSync {
-    proxy_pass http://sogo:20000/SOGo/Microsoft-Server-ActiveSync;
+    proxy_pass http://172.22.1.252:20000/SOGo/Microsoft-Server-ActiveSync;
     proxy_connect_timeout 1000;
     proxy_next_upstream timeout error;
     proxy_send_timeout 1000;
@@ -83,7 +83,7 @@ server {
   }
 
   location ^~ /SOGo {
-    proxy_pass http://sogo:20000;
+    proxy_pass http://172.22.1.252:20000;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header Host $host;
@@ -105,7 +105,7 @@ server {
   }
 
   location /SOGo.woa/WebServerResources/ {
-    proxy_pass http://sogo:9192/WebServerResources/;
+    proxy_pass http://172.22.1.252:9192/WebServerResources/;
     proxy_set_header Host $host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -115,7 +115,7 @@ server {
   }
 
   location /SOGo/WebServerResources/ {
-    proxy_pass http://sogo:9192/WebServerResources/;
+    proxy_pass http://172.22.1.252:9192/WebServerResources/;
     proxy_set_header Host $host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -125,7 +125,7 @@ server {
   }
 
   location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ {
-    proxy_pass http://sogo:9192/$1.SOGo/Resources/$2;
+    proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2;
     proxy_set_header Host $host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;

+ 6 - 1
data/conf/rspamd/dynmaps/tags.php

@@ -14,4 +14,9 @@ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 while ($row = array_shift($rows)) {
   echo strtolower(trim($row['username'])) . PHP_EOL;
 }
-?>
+$stmt = $pdo->query("SELECT CONCAT(mailbox.local_part, '@', alias_domain.alias_domain) as `tag_ad` FROM `mailbox` INNER JOIN `alias_domain` ON mailbox.domain = alias_domain.target_domain WHERE mailbox.wants_tagged_subject='1';");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+  echo strtolower(trim($row['tag_ad'])) . PHP_EOL;
+}
+?>

+ 22 - 8
data/conf/rspamd/lua/rspamd.local.lua

@@ -27,11 +27,25 @@ rspamd_config.ADD_DELIMITER_TAG = {
   callback = function(task)
     local util = require("rspamd_util")
     local rspamd_logger = require "rspamd_logger"
-    local user_tagged = task:get_recipients(1)[1]['user']
+
+    local user_env_tagged = task:get_recipients(1)[1]['user']
+    local user_to_tagged = task:get_recipients(2)[1]['user']
+
     local domain = task:get_recipients(1)[1]['domain']
-    local user, tag = user_tagged:match("([^+]+)+(.*)")
+
+    local user_env, tag_env = user_env_tagged:match("([^+]+)+(.*)")
+    local user_to, tag_to = user_to_tagged:match("([^+]+)+(.*)")
+
     local authdomain = auth_domain_map:get_key(domain)
 
+    if tag_env then
+      tag = tag_env
+      user = user_env
+    elseif tag_to then
+      tag = tag_to
+      user = user_env
+    end
+
     if tag and authdomain then
       rspamd_logger.infox("Domain %s is part of mailcow, start reading tag settings", domain)
       local user_untagged = user .. '@' .. domain
@@ -40,12 +54,12 @@ rspamd_config.ADD_DELIMITER_TAG = {
         rspamd_logger.infox("User wants subject modified for tagged mail")
         local sbj = task:get_header('Subject')
         if tag then
-        rspamd_logger.infox("Found tag %1, will modify subject header", tag)
-        new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
-        task:set_rmilter_reply({
-          remove_headers = {['Subject'] = 1},
-          add_headers = {['Subject'] = new_sbj}
-        })
+          rspamd_logger.infox("Found tag %1, will modify subject header", tag)
+          new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
+          task:set_rmilter_reply({
+            remove_headers = {['Subject'] = 1},
+            add_headers = {['Subject'] = new_sbj}
+          })
         end
       else
         rspamd_logger.infox("Add X-Moo-Tag header")

+ 18 - 3
data/web/admin.php

@@ -4,6 +4,7 @@ require_once("inc/prerequisites.inc.php");
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
 require_once("inc/header.inc.php");
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+$tfa_data = get_tfa();
 ?>
 <div class="container">
   <h4><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?=$lang['admin']['access'];?></h4>
@@ -43,12 +44,26 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
         <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>
+            <p id="tfa_pretty"><?=$tfa_data['pretty'];?></p>
+              <div id="tfa_additional">
+                <?php if($tfa_data['additional']):
+                foreach ($tfa_data['additional'] as $key_info): ?>
+                <form style="display:inline;" method="post">
+                  <input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
+                  <div style="padding:4px;margin:4px" class="label label-<?=($_SESSION['tfa_id'] == $key_info['id']) ? 'success' : 'default'; ?>">
+                  <?=$key_info['key_id'];?>
+                  <a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=strtolower($lang['admin']['remove']);?>]</a>
+                  </div>
+                </form>
+                <?php endforeach;
+                endif;?>
+              </div>
+              <br />
           </div>
         </div>
         <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">
+          <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
+          <div class="col-sm-9 col-xs-7">
             <select data-width="auto" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
               <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
               <option value="u2f"><?=$lang['tfa']['u2f'];?></option>

+ 1 - 3
data/web/autodiscover.php

@@ -1,6 +1,4 @@
-<?php
 require_once 'inc/vars.inc.php';
-
 ini_set('error_reporting', '0');
 $config = array(
      'useEASforOutlook' => 'yes',
@@ -31,7 +29,7 @@ if ($config['useEASforOutlook'] == 'no') {
 		$config['autodiscoverType'] = 'imap';
 	}
 }
-require_once 'inc/functions.inc.php';
+
 $dsn = "$database_type:host=$database_host;dbname=$database_name";
 $opt = [
 		PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,

File diff suppressed because it is too large
+ 5 - 0
data/web/css/bootstrap-select.min.css


File diff suppressed because it is too large
+ 40 - 0
data/web/css/bootstrap-slider.min.css


File diff suppressed because it is too large
+ 9 - 0
data/web/css/bootstrap-switch.min.css


+ 1 - 1
data/web/edit.php

@@ -114,7 +114,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">
-							<label><input type="checkbox" name="delete_tfa"> <?=$lang['tfa']['delete_tfa'];?></label>
+							<label><input type="checkbox" name="disable_tfa"> <?=$lang['tfa']['disable_tfa'];?></label>
 							</div>
 						</div>
 					</div>

+ 4 - 3
data/web/inc/footer.inc.php

@@ -24,9 +24,9 @@ endif;
 ?>
 <div style="margin-bottom:100px"></div>
 <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.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-select/1.9.4/js/bootstrap-select.js"></script>
+<script src="/js/bootstrap-switch.min.js"></script>
+<script src="/js/bootstrap-slider.min.js"></script>
+<script src="/js/bootstrap-select.min.js"></script>
 <script src="/js/u2f-api.js"></script>
 <script>
 // Select language and reopen active URL without POST
@@ -74,6 +74,7 @@ $(document).ready(function() {
   <?php endif; ?>
 
   // Set TFA modals
+
   $('#selectTFA').change(function () {
     if ($(this).val() == "yubi_otp") {
       $('#YubiOTPModal').modal('show');

+ 114 - 32
data/web/inc/functions.inc.php

@@ -63,6 +63,7 @@ function hasMailboxObjectAccess($username, $role, $object) {
 	return false;
 }
 function init_db_schema() {
+  // This will be much better in future releases...
 	global $pdo;
 	try {
 		$stmt = $pdo->prepare("SELECT NULL FROM `admin`, `imapsync`, `tfa`");
@@ -101,7 +102,7 @@ function init_db_schema() {
   $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'kind'");
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
   if ($num_results == 0) {
-    $pdo->query("ALTER TABLE `mailbox` ADD `kind` varchar(100) NOT NULL DEFAULT ''");
+    $pdo->query("ALTER TABLE `mailbox` ADD `kind` VARCHAR(100) NOT NULL DEFAULT ''");
   }
   $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'multiple_bookings'");
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -113,6 +114,11 @@ function init_db_schema() {
   if ($num_results == 0) {
     $pdo->query("ALTER TABLE `mailbox` ADD `wants_tagged_subject` tinyint(1) NOT NULL DEFAULT '0'");
   }
+  $stmt = $pdo->query("SHOW COLUMNS FROM `tfa` LIKE 'key_id'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results == 0) {
+    $pdo->query("ALTER TABLE `tfa` ADD `key_id` VARCHAR(255) DEFAULT 'unidentified'");
+  }
 }
 function verify_ssha256($hash, $password) {
 	// Remove tag if any
@@ -198,6 +204,8 @@ function check_login($user, $pass) {
       }
       else {
         unset($_SESSION['ldelay']);
+        $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+        $stmt->execute(array(':user' => $user));
         return "domainadmin";
       }
 		}
@@ -1806,6 +1814,10 @@ function set_tfa($postarray) {
   
 	switch ($postarray["tfa_method"]) {
 		case "yubi_otp":
+      (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"];
+      $yubico_id = $postarray['yubico_id'];
+      $yubico_key = $postarray['yubico_key'];
+      $yubi = new Auth_Yubico($yubico_id, $yubico_key);
       if (!$yubi) {
         $_SESSION['return'] = array(
           'type' => 'danger',
@@ -1824,16 +1836,21 @@ function set_tfa($postarray) {
       if (PEAR::isError($yauth)) {
 				$_SESSION['return'] = array(
 					'type' => 'danger',
-					'msg' => 'Yubico Authentication error: ' . $yauth->getMessage()
+					'msg' => 'Yubico API: ' . $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));
+        // We could also do a modhex translation here
+        $yubico_modhex_id = substr($postarray["otp_token"], 0, 12);
+        $stmt = $pdo->prepare("DELETE FROM `tfa` 
+          WHERE `username` = :username
+            AND (`authmech` != 'yubi_otp')
+            OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
+				$stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+				$stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
+					(:key_id, :username, 'yubi_otp', '1', :secret)");
+				$stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
 			}
 			catch (PDOException $e) {
 				$_SESSION['return'] = array(
@@ -1850,9 +1867,12 @@ function set_tfa($postarray) {
 
 		case "u2f":
       try {
+        (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"];
         $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));
+        $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");
+				$stmt->execute(array(':username' => $username));
+        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");
+        $stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
         $_SESSION['return'] = array(
           'type' => 'success',
           'msg' => sprintf($lang['success']['object_modified'], $username)
@@ -1887,6 +1907,55 @@ function set_tfa($postarray) {
 		break;
 	}
 }
+function unset_tfa_key($postarray) {
+  // Can only unset own keys
+  // Needs at least one key left
+  global $pdo;
+  global $lang;
+  $id = intval($postarray['unset_tfa_key']);
+  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'];
+  try {
+    if (!is_numeric($id)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
+      WHERE `username` = :username AND `active` = '1'");
+    $stmt->execute(array(':username' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if ($row['keys'] == "1") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['last_key'])
+      );
+      return false;
+    }
+    $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
+    $stmt->execute(array(':username' => $username, ':id' => $id));
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['object_modified'], $username)
+    );
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+}
 function get_tfa($username = null) {
 	global $pdo;
   if (isset($_SESSION['mailcow_cc_username'])) {
@@ -1896,8 +1965,8 @@ function get_tfa($username = null) {
     return false;
   }
 
-  $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
-      WHERE `username` = :username");
+  $stmt = $pdo->prepare("SELECT * FROM `tfa`
+      WHERE `username` = :username AND `active` = '1'");
   $stmt->execute(array(':username' => $username));
   $row = $stmt->fetch(PDO::FETCH_ASSOC);
   
@@ -1905,11 +1974,27 @@ function get_tfa($username = null) {
 		case "yubi_otp":
       $data['name'] = "yubi_otp";
       $data['pretty'] = "Yubico OTP";
+      $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
+      $stmt->execute(array(
+        ':username' => $username,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $data['additional'][] = $row;
+      }
       return $data;
     break;
 		case "u2f":
       $data['name'] = "u2f";
       $data['pretty'] = "Fido U2F";
+      $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
+      $stmt->execute(array(
+        ':username' => $username,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $data['additional'][] = $row;
+      }
       return $data;
     break;
 		case "hotp":
@@ -1935,7 +2020,7 @@ function verify_tfa_login($username, $token) {
 	global $yubi;
 
   $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
-      WHERE `username` = :username");
+      WHERE `username` = :username AND `active` = '1'");
   $stmt->execute(array(':username' => $username));
   $row = $stmt->fetch(PDO::FETCH_ASSOC);
   
@@ -1944,6 +2029,16 @@ function verify_tfa_login($username, $token) {
 			if (!ctype_alnum($token) || strlen($token) != 44) {
         return false;
       }
+      $yubico_modhex_id = substr($token, 0, 12);
+      $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
+          WHERE `username` = :username
+          AND `authmech` = 'yubi_otp'
+          AND `active`='1'
+          AND `secret` LIKE :modhex");
+      $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+      $row = $stmt->fetch(PDO::FETCH_ASSOC);
+      $yubico_auth = explode(':', $row['secret']);
+      $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
       $yauth = $yubi->verify($token);
       if (PEAR::isError($yauth)) {
 				$_SESSION['return'] = array(
@@ -1953,6 +2048,7 @@ function verify_tfa_login($username, $token) {
 				return false;
       }
       else {
+        $_SESSION['tfa_id'] = $row['id'];
         return true;
       }
     return false;
@@ -1963,6 +2059,7 @@ function verify_tfa_login($username, $token) {
       $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['tfa_id'] = $reg->id;
       $_SESSION['authReq'] = null;
       return true;
     }
@@ -2089,8 +2186,8 @@ function edit_domain_admin($postarray) {
           ':modified' => date('Y-m-d H:i:s'),
           ':active' => $active
         ));
-        if (isset($postarray['delete_tfa'])) {
-          $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+        if (isset($postarray['disable_tfa'])) {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
           $stmt->execute(array(':username' => $username_now));
         }
         else {
@@ -2115,8 +2212,8 @@ function edit_domain_admin($postarray) {
           ':modified' => date('Y-m-d H:i:s'),
           ':active' => $active
         ));
-        if (isset($postarray['delete_tfa'])) {
-          $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+        if (isset($postarray['disable_tfa'])) {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
           $stmt->execute(array(':username' => $username));
         }
         else {
@@ -4818,23 +4915,8 @@ function mailbox_get_sender_acl_handles($mailbox) {
 }
 function get_u2f_registrations($username) {
   global $pdo;
-  $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `username` = ?");
+  $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");
   $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));
-}
 ?>

+ 4 - 3
data/web/inc/header.inc.php

@@ -12,9 +12,9 @@
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.min.js" integrity="sha384-XxcvoeNF5V0ZfksTnV+bejnCsJjOOIzN6UVwF85WBsAnU3zeYh5bloN+L4WLgeNE" crossorigin="anonymous"></script>
 <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css">
 <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/<?=strtolower(trim($DEFAULT_THEME));?>/bootstrap.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.4/css/bootstrap-select.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/7.0.2/css/bootstrap-slider.min.css">
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-switch/3.3.2/css/bootstrap3/bootstrap-switch.min.css">
+<link rel="stylesheet" href="/css/bootstrap-select.min.css">
+<link rel="stylesheet" href="/css/bootstrap-slider.min.css">
+<link rel="stylesheet" href="/css/bootstrap-switch.min.css">
 <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700&subset=latin,latin-ext">
 <link rel="stylesheet" href="/inc/languages.min.css">
 <link rel="stylesheet" href="/css/mailcow.css">
@@ -45,6 +45,7 @@
 					<ul class="dropdown-menu" role="menu">
 						<li <?=($_SESSION['mailcow_locale'] == 'de') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "de"))) ?>"><span class="lang-xs lang-lbl-full" lang="de"></span></a></li>
 						<li <?=($_SESSION['mailcow_locale'] == 'en') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "en"))) ?>"><span class="lang-xs lang-lbl-full" lang="en"></span></a></li>
+						<li <?=($_SESSION['mailcow_locale'] == 'es') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "es"))) ?>"><span class="lang-xs lang-lbl-full" lang="es"></span></a></li>
 						<li <?=($_SESSION['mailcow_locale'] == 'nl') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "nl"))) ?>"><span class="lang-xs lang-lbl-full" lang="nl"></span></a></li>
 						<li <?=($_SESSION['mailcow_locale'] == 'pt') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "pt"))) ?>"><span class="lang-xs lang-lbl-full" lang="pt"></span></a></li>
 					</ul>

+ 10 - 4
data/web/inc/prerequisites.inc.php

@@ -22,10 +22,8 @@ if (file_exists('./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']);
-}
+require_once 'inc/lib/Yubico.php';
+
 // U2F API
 require_once 'inc/lib/U2F.php';
 $scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://";
@@ -59,6 +57,10 @@ if (isset($_COOKIE['language'])) {
 			$_SESSION['mailcow_locale'] = 'en';
 			setcookie('language', 'en');
 		break;
+		case "es":
+			$_SESSION['mailcow_locale'] = 'es';
+			setcookie('language', 'es');
+		break;
 		case "nl":
 			$_SESSION['mailcow_locale'] = 'nl';
 			setcookie('language', 'nl');
@@ -79,6 +81,10 @@ if (isset($_GET['lang'])) {
 			$_SESSION['mailcow_locale'] = 'en';
 			setcookie('language', 'en');
 		break;
+		case "es":
+			$_SESSION['mailcow_locale'] = 'es';
+			setcookie('language', 'es');
+		break;
 		case "nl":
 			$_SESSION['mailcow_locale'] = 'nl';
 			setcookie('language', 'nl');

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

@@ -4,6 +4,18 @@
       <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="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
+        </div>
+        <hr>
+        <p class="help-block"><?=$lang['tfa']['api_register'];?></p>
+        <div class="form-group">
+          <input type="text" class="form-control" name="yubico_id" id="yubico_id" placeholder="Yubico API ID" autocomplete="off" required>
+        </div>
+        <div class="form-group">
+          <input type="text" class="form-control" name="yubico_key" id="yubico_key" placeholder="Yubico API Key" autocomplete="off" required>
+        </div>
+        <hr>
         <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>
@@ -27,6 +39,9 @@
       <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="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
+          </div>
           <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>

+ 3 - 0
data/web/inc/triggers.inc.php

@@ -115,6 +115,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 	if (isset($_POST["set_tfa"])) {
 		set_tfa($_POST);
 	}
+	if (isset($_POST["unset_tfa_key"])) {
+		unset_tfa_key($_POST);
+	}
 	if (isset($_POST["add_policy_list_item"])) {
 		add_policy_list_item($_POST);
 	}

+ 0 - 4
data/web/inc/vars.inc.php

@@ -35,8 +35,4 @@ $DEFAULT_LANG = "en";
 // See https://bootswatch.com/
 $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'] = "";
 ?>

+ 1 - 0
data/web/index.php

@@ -48,6 +48,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 								<ul class="dropdown-menu">
 									<li <?=($_SESSION['mailcow_locale'] == 'de') ? 'class="active"' : ''?>><a href="?<?= http_build_query(array_merge($_GET, array("lang" => "de"))) ?>"><span class="lang-xs lang-lbl-full" lang="de"></span></a></li>
 									<li <?=($_SESSION['mailcow_locale'] == 'en') ? 'class="active"' : ''?>><a href="?<?= http_build_query(array_merge($_GET, array("lang" => "en"))) ?>"><span class="lang-xs lang-lbl-full" lang="en"></span></a></li>
+									<li <?=($_SESSION['mailcow_locale'] == 'es') ? 'class="active"' : ''?>><a href="?<?= http_build_query(array_merge($_GET, array("lang" => "es"))) ?>"><span class="lang-xs lang-lbl-full" lang="es"></span></a></li>
 									<li <?=($_SESSION['mailcow_locale'] == 'nl') ? 'class="active"' : ''?>><a href="?<?= http_build_query(array_merge($_GET, array("lang" => "nl"))) ?>"><span class="lang-xs lang-lbl-full" lang="nl"></span></a></li>
 									<li <?=($_SESSION['mailcow_locale'] == 'pt') ? 'class="active"' : ''?>><a href="?<?= http_build_query(array_merge($_GET, array("lang" => "pt"))) ?>"><span class="lang-xs lang-lbl-full" lang="pt"></span></a></li>
 								</ul>

File diff suppressed because it is too large
+ 6 - 0
data/web/js/bootstrap-select.min.js


File diff suppressed because it is too large
+ 3 - 0
data/web/js/bootstrap-slider.min.js


File diff suppressed because it is too large
+ 9 - 0
data/web/js/bootstrap-switch.min.js


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

@@ -29,6 +29,7 @@ $lang['danger']['policy_list_from_exists'] = 'Ein Eintrag mit diesem Wert existi
 $lang['danger']['policy_list_from_invalid'] = 'Eintrag hat ungültiges Format';
 $lang['danger']['alias_invalid'] = 'Alias-Adrese ist ungültig';
 $lang['danger']['goto_invalid'] = 'Ziel-Adrese ist ungültig';
+$lang['danger']['last_key'] = 'Letzter Key kann nicht gelöscht werden';
 $lang['danger']['alias_domain_invalid'] = 'Alias-Domain ist ungültig';
 $lang['danger']['target_domain_invalid'] = 'Ziel-Domain ist ungültig';
 $lang['danger']['object_exists'] = 'Objekt %s existiert bereits';
@@ -374,11 +375,14 @@ $lang['login']['delayed'] = 'Login wurde zur Sicherheit um %s Sekunde/n verzöge
 $lang['tfa']['tfa'] = "Two-Factor Authentication";
 $lang['tfa']['set_tfa'] = "Konfiguriere Two-Factor Authentication Methode";
 $lang['tfa']['yubi_otp'] = "Yubico OTP Authentifizierung";
+$lang['tfa']['key_id'] = "Ein Name für diesen YubiKey";
+$lang['tfa']['api_register'] = 'mailcow verwendet die Yubico Cloud API. Ein API-Key für den Yubico Stick kann <a href="https://upgrade.yubico.com/getapikey/" target="_blank">hier</a> bezogen werden.';
 $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']['disable_tfa'] = "Deaktiviere TFA bis zur nächsten erfolgreichen Anmeldung";
 $lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field";
 $lang['tfa']['confirm'] = "Bestätigen";
 $lang['tfa']['otp'] = "Einmalpasswort";

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

@@ -24,6 +24,7 @@ $lang['danger']['mailbox_quota_exceeds_domain_quota'] = "Max. quota exceeds doma
 $lang['danger']['object_is_not_numeric'] = "Value %s is not numeric";
 $lang['success']['domain_added'] = "Added domain %s";
 $lang['danger']['alias_empty'] = "Alias address must not be empty";
+$lang['danger']['last_key'] = 'Last key cannot be deleted';
 $lang['danger']['goto_empty'] = "Goto address must not be empty";
 $lang['danger']['policy_list_from_exists'] = "A record with given name exists";
 $lang['danger']['policy_list_from_invalid'] = "Record has invalid format";
@@ -377,11 +378,14 @@ $lang['login']['delayed'] = 'Login was delayed by %s seconds.';
 $lang['tfa']['tfa'] = "Two-factor authentication";
 $lang['tfa']['set_tfa'] = "Set two-factor authentication method";
 $lang['tfa']['yubi_otp'] = "Yubico OTP authentication";
+$lang['tfa']['key_id'] = "An identifier for your YubiKey";
+$lang['tfa']['api_register'] = 'mailcow uses the Yubico Cloud API. Please get an API key for your key <a href="https://upgrade.yubico.com/getapikey/" target="_blank">here</a>';
 $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']['disable_tfa'] = "Disable TFA until next successful login";
 $lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field";
 $lang['tfa']['confirm'] = "Confirm";
 $lang['tfa']['otp'] = "One-time password";

+ 16 - 3
data/web/user.php

@@ -8,6 +8,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
 
 	require_once("inc/header.inc.php");
 	$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+  $tfa_data = get_tfa();
 	$username = $_SESSION['mailcow_cc_username'];
 ?>
 <div class="container">
@@ -23,15 +24,27 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
     <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 class="col-sm-9 col-xs-7">
+          <p id="tfa_pretty"><?=$tfa_data['pretty'];?></p>
+            <div id="tfa_additional">
+              <?php if($tfa_data['additional']):
+              foreach ($tfa_data['additional'] as $key_info): ?>
+                <form style="display:inline;" method="post">
+                <input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
+                <div class="label label-default">🔑 <?=$key_info['key_id'];?> <a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=strtolower($lang['admin']['remove']);?>]</a></div>
+              </form>
+              <?php endforeach;
+              endif;?>
+            </div>
+            <br />
+        </div>
     </div>
     <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="u2f"><?=$lang['tfa']['u2f'];?></option>
           <option value="none"><?=$lang['tfa']['none'];?></option>
         </select>
       </div>

+ 11 - 7
docker-compose.yml

@@ -3,6 +3,9 @@ version: '2.1'
 services:
     pdns-mailcow:
       image: andryyy/mailcow-dockerized:pdns
+      depends_on:
+        mysql-mailcow:
+          condition: service_healthy
       volumes:
         - ./data/conf/pdns/:/etc/powerdns/
       restart: always
@@ -14,9 +17,11 @@ services:
 
     mysql-mailcow:
       image: mariadb:10.1
-      depends_on:
-        - pdns-mailcow
-      command: mysqld
+      healthcheck:
+        test: ["CMD", "mysqladmin", "ping", "--host", "localhost", "--silent"]
+        interval: 10s
+        timeout: 30s
+        retries: 5
       volumes:
         - mysql-vol-1:/var/lib/mysql/
         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
@@ -52,7 +57,7 @@ services:
     rspamd-mailcow:
       image: andryyy/mailcow-dockerized:rspamd
       depends_on:
-        - pdns-mailcow
+        - nginx-mailcow
       volumes:
         - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:ro
         - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:ro
@@ -65,6 +70,7 @@ services:
       dns_search: mailcow-network
       networks:
         mailcow-network:
+          ipv4_address: 172.22.1.253
           aliases:
             - rspamd
 
@@ -95,7 +101,6 @@ services:
       image: andryyy/mailcow-dockerized:sogo
       depends_on:
         - pdns-mailcow
-        - mysql-mailcow
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -110,6 +115,7 @@ services:
       restart: always
       networks:
         mailcow-network:
+          ipv4_address: 172.22.1.252
           aliases:
             - sogo
 
@@ -197,10 +203,8 @@ services:
 
     nginx-mailcow:
       depends_on:
-        - mysql-mailcow
         - sogo-mailcow
         - php-fpm-mailcow
-        - rspamd-mailcow
       image: nginx:mainline
       command: /bin/bash -c "envsubst < /etc/nginx/conf.d/listen.template > /etc/nginx/conf.d/listen.active && nginx -g 'daemon off;'"
       environment:

Some files were not shown because too many files changed in this diff