Browse Source

Merge branch 'dev'

Conflicts:
	data/web/autodiscover.php
andryyy 8 years ago
parent
commit
aa5b03dd99

+ 3 - 0
.gitignore

@@ -13,3 +13,6 @@ data/assets/ssl/*
 data/web/.well-known/acme-challenge
 data/conf/rspamd/local.d/*
 data/conf/rspamd/override.d/*
+!data/conf/nginx/dynmaps.conf
+!data/conf/nginx/site.conf
+data/conf/nginx/*.conf

+ 16 - 0
.travis.yml

@@ -0,0 +1,16 @@
+sudo: required
+services:
+- docker
+script:
+- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh
+- docker-compose pull --ignore-pull-failures --parallel
+- docker-compose build
+- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD
+- docker-compose push
+branches:
+  only:
+  - master
+env:
+  global:
+  - secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w=
+  - secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo=

+ 3 - 1
data/Dockerfiles/acme/docker-entrypoint.sh

@@ -36,6 +36,8 @@ else
 	fi
 fi
 
+[[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem
+
 while true; do
 	if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
 		echo "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
@@ -44,7 +46,7 @@ while true; do
 	declare -a SQL_DOMAIN_ARR
     declare -a VALIDATED_CONFIG_DOMAINS
 	declare -a ADDITIONAL_VALIDATED_SAN
-	IFS=' ' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}"
+	IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}"
 	IPV4=$(curl -4s https://mailcow.email/ip.php)
 
 	while read line; do

+ 38 - 5
data/Dockerfiles/fail2ban/logwatch.py

@@ -1,6 +1,7 @@
 #!/usr/bin/env python2
 
 import re
+import os
 import time
 import atexit
 import signal
@@ -12,7 +13,12 @@ import redis
 import time
 import json
 
-r = redis.StrictRedis(host='172.22.1.249', port=6379, db=0)
+yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$')
+if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
+	print "Skipping Fail2ban container..."
+	raise SystemExit
+
+r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
 RULES = {
 	'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
 	'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
@@ -32,6 +38,7 @@ def ban(address):
 	BAN_TIME = int(r.get("F2B_BAN_TIME"))
 	MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
 	RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
+	WHITELIST = r.hgetall("F2B_WHITELIST")
 
 	ip = ipaddress.ip_address(address.decode('ascii'))
 	if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
@@ -39,7 +46,19 @@ def ban(address):
 		address = str(ip)
 	if ip.is_private or ip.is_loopback:
 		return
-	
+
+	self_network = ipaddress.ip_network(address.decode('ascii'))
+	if WHITELIST:
+		for wl_key in WHITELIST:
+			wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
+			if wl_net.overlaps(self_network):
+				log['time'] = int(round(time.time()))
+				log['priority'] = "info"
+				log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
+				r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+				print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
+				return
+
 	net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
 	net = str(net)
 
@@ -48,10 +67,10 @@ def ban(address):
 		active_window = RETRY_WINDOW
 	else:
 		active_window = time.time() - bans[net]['last_attempt']
-	
+
 	bans[net]['attempts'] += 1
 	bans[net]['last_attempt'] = time.time()
-	
+
 	active_window = time.time() - bans[net]['last_attempt']
 
 	if bans[net]['attempts'] >= MAX_ATTEMPTS:
@@ -66,6 +85,7 @@ def ban(address):
 		else:
 			subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
 			subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
+		r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
 	else:
 		log['time'] = int(round(time.time()))
 		log['priority'] = "warn"
@@ -76,6 +96,13 @@ def ban(address):
 def unban(net):
 	log['time'] = int(round(time.time()))
 	log['priority'] = "info"
+	r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+	if not net in bans:
+		log['message'] = "%s is not banned, skipping unban and deleting from queue (if any)" % net
+		r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+		print "%s is not banned, skipping unban and deleting from queue (if any)" % net
+		r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
+		return
 	log['message'] = "Unbanning %s" % net
 	r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
 	print "Unbanning %s" % net
@@ -85,6 +112,8 @@ def unban(net):
 	else:
 		subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
 		subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
+	r.hdel("F2B_ACTIVE_BANS", "%s" % net)
+	r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
 	del bans[net]
 
 def quit(signum, frame):
@@ -117,11 +146,15 @@ def autopurge():
 	while not quit_now:
 		BAN_TIME = int(r.get("F2B_BAN_TIME"))
 		MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
+		QUEUE_UNBAN = r.hgetall("F2B_QUEUE_UNBAN")
+		if QUEUE_UNBAN:
+			for net in QUEUE_UNBAN:
+				unban(str(net))
 		for net in bans.copy():
 			if bans[net]['attempts'] >= MAX_ATTEMPTS:
 				if time.time() - bans[net]['last_attempt'] > BAN_TIME:
 					unban(net)
-		time.sleep(60)
+		time.sleep(30)
 
 if __name__ == '__main__':
 	threads = []

+ 1 - 9
data/conf/rspamd/lua/rspamd.local.lua

@@ -7,9 +7,6 @@ rspamd_config.MAILCOW_AUTH = {
 	end
 }
 
-local redis_params
-redis_params = rspamd_parse_redis_server('tag_settings')
-if redis_params then
 rspamd_config:register_symbol({
   name = 'TAG_MOO',
   type = 'postfilter',
@@ -20,11 +17,6 @@ rspamd_config:register_symbol({
     local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
     local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
 
-    local user = task:get_recipients(0)[1]['user']
-    local domain = task:get_recipients(0)[1]['domain']
-    local rcpt = user .. '@' .. domain
-
-
     if tagged_rcpt and mailcow_domain then
       local tag = tagged_rcpt[1].options[1]
       rspamd_logger.infox("found tag: %s", tag)
@@ -57,5 +49,5 @@ rspamd_config:register_symbol({
   end,
   priority = 11
 })
-end
+
 

+ 28 - 0
data/web/admin.php

@@ -269,6 +269,34 @@ $tfa_data = get_tfa();
         </form>
       </div>
     </div>
+
+    <div class="panel panel-default">
+      <div class="panel-heading">Fail2Ban parameters</div>
+      <div class="panel-body">
+      <?php
+      $f2b_data = get_f2b_parameters();
+      ?>
+        <form class="form" data-id="f2b" role="form" method="post">
+          <div class="form-group">
+            <label for="ban_time">Ban time (s):</label>
+            <input type="number" class="form-control" id="ban_time" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required>
+          </div>
+          <div class="form-group">
+            <label for="max_attempts">Max. attempts:</label>
+            <input type="number" class="form-control" id="max_attempts" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required>
+          </div>
+          <div class="form-group">
+            <label for="retry_window">Retry window (s) for max. attempts:</label>
+            <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
+          </div>
+          <div class="form-group">
+            <label for="retry_window">Whitelisted networks/hosts</label>
+            <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
+          </div>
+          <button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+        </form>
+      </div>
+    </div>
   </div>
 
   <div role="tabpanel" class="tab-pane" id="tab-postfix-logs">

+ 0 - 164
data/web/autodiscover.php

@@ -1,164 +0,0 @@
-<?php
-require_once 'inc/vars.inc.php';
-require_once 'inc/functions.inc.php';
-$config = array(
-     'useEASforOutlook' => 'yes',
-     'autodiscoverType' => 'activesync',
-     'imap' => array(
-       'server' => $mailcow_hostname,
-       'port' => '993',
-       'ssl' => 'on',
-     ),
-     'smtp' => array(
-       'server' => $mailcow_hostname,
-       'port' => '465',
-       'ssl' => 'on'
-     ),
-     'activesync' => array(
-       'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync'
-     )
-);
-
-if(file_exists('inc/vars.local.inc.php')) {
-	include_once 'inc/vars.local.inc.php';
-}
-
-/* ---------- DO NOT MODIFY ANYTHING BEYOND THIS LINE. IGNORE AT YOUR OWN RISK. ---------- */
-
-error_reporting(0);
-
-$data = trim(file_get_contents("php://input"));
-
-// Desktop client needs IMAP, unless it's Outlook 2013 or higher on Windows
-if (strpos($data, 'autodiscover/outlook/responseschema')) { // desktop client
-	$config['autodiscoverType'] = 'imap';
-	if ($config['useEASforOutlook'] == 'yes' &&
-	    strpos($_SERVER['HTTP_USER_AGENT'], 'Windows NT') !== FALSE && // Windows
-	    preg_match('/(Outlook|Office) (1[5-9]\.|[2-9]|1[0-9][0-9])/', $_SERVER['HTTP_USER_AGENT']) && // Outlook 2013 (version 15) or higher
-	    strpos($_SERVER['HTTP_USER_AGENT'], 'MS Connectivity Analyzer') === FALSE // https://testconnectivity.microsoft.com doesn't support EAS for Outlook
-	) {
-			$config['autodiscoverType'] = 'activesync';
-	}
-}
-
-$dsn = "$database_type:host=$database_host;dbname=$database_name";
-$opt = [
-		PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
-		PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
-		PDO::ATTR_EMULATE_PREPARES   => false,
-];
-$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
-$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
-$as = check_login($login_user, $_SERVER['PHP_AUTH_PW']);
-
-if (!isset($_SERVER['PHP_AUTH_USER']) OR $as !== "user") {
-	header('WWW-Authenticate: Basic realm=""');
-	header('HTTP/1.0 401 Unauthorized');
-	exit;
-} else {
-	if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
-		if ($as === "user") {
-      header("Content-Type: application/xml");
-      echo '<?xml version="1.0" encoding="utf-8" ?><Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">';
-
-      if(!$data) {
-        list($usec, $sec) = explode(' ', microtime());
-        echo '<Response>';
-        echo '<Error Time="' . date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2) . '" Id="2477272013">';
-        echo '<ErrorCode>600</ErrorCode><Message>Invalid Request</Message><DebugData /></Error>';
-        echo '</Response>';
-        echo '</Autodiscover>';
-        exit(0);
-      }
-      $discover = new SimpleXMLElement($data);
-      $email = $discover->Request->EMailAddress;
-
-      if ($config['autodiscoverType'] == 'imap') {
-      ?>
-  <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
-      <User>
-          <DisplayName><?php echo $displayname; ?></DisplayName>
-      </User>
-      <Account>
-          <AccountType>email</AccountType>
-          <Action>settings</Action>
-          <Protocol>
-              <Type>IMAP</Type>
-              <Server><?php echo $config['imap']['server']; ?></Server>
-              <Port><?php echo $config['imap']['port']; ?></Port>
-              <DomainRequired>off</DomainRequired>
-              <LoginName><?php echo $email; ?></LoginName>
-              <SPA>off</SPA>
-              <SSL><?php echo $config['imap']['ssl']; ?></SSL>
-              <AuthRequired>on</AuthRequired>
-          </Protocol>
-          <Protocol>
-              <Type>SMTP</Type>
-              <Server><?php echo $config['smtp']['server']; ?></Server>
-              <Port><?php echo $config['smtp']['port']; ?></Port>
-              <DomainRequired>off</DomainRequired>
-              <LoginName><?php echo $email; ?></LoginName>
-              <SPA>off</SPA>
-              <SSL><?php echo $config['smtp']['ssl']; ?></SSL>
-              <AuthRequired>on</AuthRequired>
-              <UsePOPAuth>on</UsePOPAuth>
-              <SMTPLast>off</SMTPLast>
-          </Protocol>
-          <Protocol>
-              <Type>CalDAV</Type>
-              <Server>https://<?php echo $mailcow_hostname; ?>/SOGo/dav/<?php echo $email; ?>/Calendar</Server>
-              <DomainRequired>off</DomainRequired>
-              <LoginName><?php echo $email; ?></LoginName>
-          </Protocol>
-          <Protocol>
-              <Type>CardDAV</Type>
-              <Server>https://<?php echo $mailcow_hostname; ?>/SOGo/dav/<?php echo $email; ?>/Contacts</Server>
-              <DomainRequired>off</DomainRequired>
-              <LoginName><?php echo $email; ?></LoginName>
-          </Protocol>
-      </Account>
-  </Response>
-      <?php
-      }
-      else if ($config['autodiscoverType'] == 'activesync') {
-        $username = trim($email);
-        try {
-          $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
-          $stmt->execute(array(':username' => $username));
-          $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
-        }
-        catch(PDOException $e) {
-          die("Failed to determine name from SQL");
-        }
-        if (!empty($MailboxData['name'])) {
-          $displayname = utf8_encode($MailboxData['name']);
-        }
-        else {
-          $displayname = $email;
-        }
-      ?>
-  <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006">
-      <Culture>en:en</Culture>
-      <User>
-          <DisplayName><?php echo $displayname; ?></DisplayName>
-          <EMailAddress><?php echo $email; ?></EMailAddress>
-      </User>
-      <Action>
-          <Settings>
-              <Server>
-                  <Type>MobileSync</Type>
-                  <Url><?php echo $config['activesync']['url']; ?></Url>
-                  <Name><?php echo $config['activesync']['url']; ?></Name>
-              </Server>
-          </Settings>
-      </Action>
-  </Response>
-      <?php
-      }
-      ?>
-</Autodiscover>
-<?php
-		}
-	}
-}
-?>

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

@@ -11,6 +11,11 @@ table.footable>tbody>tr.footable-empty>td {
 .table-responsive {
   overflow: visible !important;
 }
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    overflow-x: scroll !important;
+  }
+}
 body {
   overflow-y:scroll;
 }

+ 5 - 0
data/web/css/edit.css

@@ -11,6 +11,11 @@ table.footable>tbody>tr.footable-empty>td {
 .table-responsive {
   overflow: visible !important;
 }
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    overflow-x: scroll !important;
+  }
+}
 .footer-add-item {
   display:block;
   text-align: center;

+ 5 - 1
data/web/css/mailbox.css

@@ -11,6 +11,11 @@ table.footable>tbody>tr.footable-empty>td {
 .table-responsive {
   overflow: visible !important;
 }
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    overflow-x: scroll !important;
+  }
+}
 .footer-add-item {
   display:block;
   text-align: center;
@@ -30,4 +35,3 @@ table.footable>tbody>tr.footable-empty>td {
 .inputMissingAttr {
   border-color: #FF4136;
 }
-

+ 5 - 0
data/web/css/user.css

@@ -11,6 +11,11 @@ table.footable>tbody>tr.footable-empty>td {
 .table-responsive {
   overflow: visible !important;
 }
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    overflow-x: scroll !important;
+  }
+}
 .footer-add-item {
   display:block;
   text-align: center;

+ 91 - 0
data/web/inc/functions.inc.php

@@ -229,6 +229,7 @@ function check_login($user, $pass) {
 	}
 	if (!isset($_SESSION['ldelay'])) {
 		$_SESSION['ldelay'] = "0";
+    error_log("Mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
 	}
 	elseif (!isset($_SESSION['mailcow_cc_username'])) {
 		$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
@@ -1434,4 +1435,94 @@ function get_logs($container, $lines = 100) {
   }
   return false;
 }
+function get_f2b_parameters() {
+	global $lang;
+	global $redis;
+  $data = array();
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		return false;
+	}
+  try {
+    $data['ban_time'] = $redis->Get('F2B_BAN_TIME');
+    $data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
+    $data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
+    $wl = $redis->hGetAll('F2B_WHITELIST');
+    if (is_array($wl)) {
+      foreach ($wl as $key => $value) {
+        $tmp_data[] = $key;
+      }
+      $data['whitelist'] = implode(PHP_EOL, $tmp_data);
+    }
+    else {
+      $data['whitelist'] = "";
+    }
+  }
+  catch (RedisException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'Redis: '.$e
+    );
+    return false;
+  }
+  return $data;
+}
+function edit_f2b_parameters($postarray) {
+	global $lang;
+	global $redis;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  $is_now = get_f2b_parameters();
+  if (!empty($is_now)) {
+    $ban_time = intval((isset($postarray['ban_time'])) ? $postarray['ban_time'] : $is_now['ban_time']);
+    $max_attempts = intval((isset($postarray['max_attempts'])) ? $postarray['max_attempts'] : $is_now['active_int']);
+    $retry_window = intval((isset($postarray['retry_window'])) ? $postarray['retry_window'] : $is_now['retry_window']);
+  }
+  else {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  $wl = $postarray['whitelist'];
+  $ban_time = ($ban_time < 60) ? 60 : $ban_time;
+  $max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
+  $retry_window = ($retry_window < 1) ? 1 : $retry_window;
+  try {
+    $redis->Set('F2B_BAN_TIME', $ban_time);
+    $redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
+    $redis->Set('F2B_RETRY_WINDOW', $retry_window);
+    $redis->Del('F2B_WHITELIST');
+    if(!empty($wl)) {
+      $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
+      if (is_array($wl_array)) {
+        foreach ($wl_array as $wl_item) {
+          $cidr = explode('/', $wl_item);
+          if (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 32))) {
+            $redis->hSet('F2B_WHITELIST', $wl_item, 1);
+          }
+          elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 128))) {
+            $redis->hSet('F2B_WHITELIST', $wl_item, 1);
+          }
+        }
+      }
+    }
+  }
+  catch (RedisException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'Redis: '.$e
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => 'Saved changes to Fail2ban configuration'
+  );
+}
 ?>

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

@@ -17,6 +17,28 @@ $database_name = getenv('DBNAME');
 // Other variables
 $mailcow_hostname = getenv('MAILCOW_HOSTNAME');
 
+// Autodiscover settings
+$autodiscover_config = array(
+  // Enable the autodiscover service for Outlook desktop clients
+  'useEASforOutlook' => 'yes',
+  // General autodiscover service type: "activesync" or "imap"
+  'autodiscoverType' => 'activesync',
+  'imap' => array(
+    'server' => $mailcow_hostname,
+    'port' => getenv('IMAPS_PORT'),
+    'ssl' => 'on',
+  ),
+  'smtp' => array(
+    'server' => $mailcow_hostname,
+    'port' => getenv('SMTPS_PORT'),
+    'ssl' => 'on'
+  ),
+  'activesync' => array(
+    'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync'
+  )
+);
+
+
 // Where to go after adding and editing objects
 // Can be "form" or "previous"
 // "form" will stay in the current form, "previous" will redirect to previous page

+ 35 - 0
data/web/json_api.php

@@ -1921,6 +1921,41 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "fail2ban":
+            // No items
+            if (isset($_POST['attr'])) {
+              $attr = (array)json_decode($_POST['attr'], true);
+              if (edit_f2b_parameters($attr) === false) {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'error',
+                    'msg' => 'Edit failed'
+                  ));
+                }
+                exit();
+              }
+              else {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'success',
+                    'msg' => 'Task completed'
+                  ));
+                }
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Incomplete post data'
+              ));
+            }
+          break;
           case "admin":
             // No items as there is only one admin
             if (isset($_POST['attr'])) {

+ 11 - 3
docker-compose.yml

@@ -122,6 +122,14 @@ services:
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+        - IMAP_PORT=${IMAP_PORT:-143}
+        - IMAPS_PORT=${IMAPS_PORT:-993}
+        - POP_PORT=${POP_PORT:-110}
+        - POPS_PORT=${POPS_PORT:-995}
+        - SIEVE_PORT=${SIEVE_PORT:-4190}
+        - SUBMISSION_PORT=${SUBMISSION_PORT:-587}
+        - SMTPS_PORT=${SMTPS_PORT:-465}
+        - SMTP_PORT=${SMTP_PORT:-25}
       restart: always
       logging:
         options:
@@ -285,7 +293,7 @@ services:
     acme-mailcow:
       depends_on:
         - nginx-mailcow
-      image: mailcow/acme:1.4
+      image: mailcow/acme:1.5
       build: ./data/Dockerfiles/acme
       dns:
         - 172.22.1.254
@@ -311,7 +319,7 @@ services:
             - acme
 
     fail2ban-mailcow:
-      image: mailcow/fail2ban:1.2
+      image: mailcow/fail2ban:1.3
       build: ./data/Dockerfiles/fail2ban
       depends_on:
         - dovecot-mailcow
@@ -323,6 +331,7 @@ services:
       privileged: true
       environment:
         - TZ=${TZ}
+        - SKIP_FAIL2BAN=${SKIP_FAIL2BAN:-no}
       network_mode: "host"
       dns:
         - 172.22.1.254
@@ -330,7 +339,6 @@ services:
       volumes:
         - /var/run/docker.sock:/var/run/docker.sock:ro
         - /lib/modules:/lib/modules:ro
-
     ipv6nat:
       image: robbertkl/ipv6nat
       restart: always

+ 2 - 0
generate_config.sh

@@ -81,6 +81,8 @@ ADDITIONAL_SAN=
 # To never run acme-mailcow for Let's Encrypt, set this to y
 SKIP_LETS_ENCRYPT=n
 
+# To never run fail2ban-mailcow
+SKIP_FAIL2BAN=n
 
 EOF