From 5abeb313bad795f9005367416f4edacff05a078a Mon Sep 17 00:00:00 2001 From: Michael Kuron Date: Mon, 10 Jul 2017 20:52:51 +0200 Subject: [PATCH 01/64] Autoconfig harmonization - use $autodiscover_config everywhere - non-443 HTTPS ports in autoconfig etc. - disabling POP service via SRV record - fix display name in Outlook IMAP autodiscover - allow multiple calls to TLSA generator and support Sieve STARTTLS - iOS mobileconfig generator --- data/web/autoconfig.php | 47 +++++++--- data/web/autodiscover.php | 54 ++++++----- data/web/inc/functions.inc.php | 24 +++-- data/web/inc/vars.inc.php | 31 ++++-- data/web/mobileconfig.php | 167 +++++++++++++++++++++++++++++++++ 5 files changed, 267 insertions(+), 56 deletions(-) create mode 100644 data/web/mobileconfig.php diff --git a/data/web/autoconfig.php b/data/web/autoconfig.php index d01bc724..523d6183 100644 --- a/data/web/autoconfig.php +++ b/data/web/autoconfig.php @@ -5,6 +5,16 @@ if (empty($mailcow_hostname)) { exit(); } +$domain_dot = strpos($_SERVER['HTTP_HOST'], '.'); +$domain_port = strpos($_SERVER['HTTP_HOST'], ':'); +if ($domain_port === FALSE) { + $domain = substr($_SERVER['HTTP_HOST'], $domain_dot+1); + $port = 443; +} else { + $domain = substr($_SERVER['HTTP_HOST'], $domain_dot+1, $domain_port-$domain_dot-1); + $port = substr($_SERVER['HTTP_HOST'], $domain_port+1); +} + header('Content-Type: application/xml'); ?> '; ?> @@ -15,52 +25,59 @@ header('Content-Type: application/xml'); mail server - - 993 + + SSL %EMAILADDRESS% password-cleartext - - 143 + + STARTTLS %EMAILADDRESS% password-cleartext + - - 995 + + SSL %EMAILADDRESS% password-cleartext + + - - 110 + + STARTTLS %EMAILADDRESS% password-cleartext + - - 465 + + SSL %EMAILADDRESS% password-cleartext - - - 587 + + STARTTLS %EMAILADDRESS% password-cleartext - + If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden. @@ -68,6 +85,6 @@ header('Content-Type: application/xml'); - + diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index fd8cd641..7f6081ae 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -12,13 +12,14 @@ 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 +if (strpos($data, 'autodiscover/outlook/responseschema') !== false) { // desktop client $configuration['autodiscoverType'] = 'imap'; if ($configuration['useEASforOutlook'] == 'yes' && - // Office for macOS does not support EAS - strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && - // Outlook 2013 (version 15) or higher - preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT'])) { + // Office for macOS does not support EAS + strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && + // Outlook 2013 (version 15) or higher + preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT']) + ) { $configuration['autodiscoverType'] = 'activesync'; } } @@ -60,8 +61,28 @@ else { Request->EMailAddress; + try { + $discover = new SimpleXMLElement($data); + $email = $discover->Request->EMailAddress; + } catch (Exception $e) { + $email = $_SERVER['PHP_AUTH_USER']; + } + + $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; + } if ($configuration['autodiscoverType'] == 'imap') { ?> @@ -96,13 +117,13 @@ else { CalDAV - /SOGo/dav//Calendar + https:///SOGo/dav//Calendar off CardDAV - /SOGo/dav//Contacts + https:///SOGo/dav//Contacts off @@ -111,21 +132,6 @@ else { 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; - } ?> en:en diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index b3420530..af93794b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -62,17 +62,17 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } +function pem_to_der($pem_key) { + // Need to remove BEGIN/END PUBLIC KEY + $lines = explode("\n", trim($pem_key)); + unset($lines[count($lines)-1]); + unset($lines[0]); + return base64_decode(implode('', $lines)); +} function generate_tlsa_digest($hostname, $port, $starttls = null) { if (!is_valid_domain_name($hostname)) { return "Not a valid hostname"; } - function pem_to_der($pem_key) { - // Need to remove BEGIN/END PUBLIC KEY - $lines = explode("\n", trim($pem_key)); - unset($lines[count($lines)-1]); - unset($lines[0]); - return base64_decode(implode('', $lines)); - } if (empty($starttls)) { $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); @@ -88,20 +88,24 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { return $error_nr . ': ' . $error_msg; } $banner = fread($stream, 512 ); - if (preg_match("/^220/i", $banner)) { + if (preg_match("/^220/i", $banner)) { // SMTP fwrite($stream,"HELO tlsa.generator.local\r\n"); fread($stream, 512); fwrite($stream,"STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/imap.+starttls/i", $banner)) { + elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP fwrite($stream,"A1 STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/^\+OK/", $banner)) { + elseif (preg_match("/^\+OK/", $banner)) { // POP3 fwrite($stream,"STLS\r\n"); fread($stream, 512); } + elseif (preg_match("/^OK/m", $banner)) { // Sieve + fwrite($stream,"STARTTLS\r\n"); + fread($stream, 512); + } else { return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"'; } diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index a52442db..775af1e1 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -18,31 +18,48 @@ $database_name = getenv('DBNAME'); $mailcow_hostname = getenv('MAILCOW_HOSTNAME'); // Autodiscover settings +$https_port = strpos($_SERVER['HTTP_HOST'], ':'); +if ($https_port === FALSE) { + $https_port = 443; +} else { + $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1); +} $autodiscover_config = array( // Enable the autodiscover service for Outlook desktop clients 'useEASforOutlook' => 'yes', // General autodiscover service type: "activesync" or "imap" 'autodiscoverType' => 'activesync', - // Please don't use STARTTLS-enabled service ports here. + // Please don't use STARTTLS-enabled service ports in the "port" variable. // The autodiscover service will always point to SMTPS and IMAPS (TLS-wrapped services). + // The autoconfig service will additionally announce the STARTTLS-enabled ports, specified in the "tlsport" variable. 'imap' => array( 'server' => $mailcow_hostname, - 'port' => getenv('IMAPS_PORT'), + 'port' => array_pop(explode(':', getenv('IMAPS_PORT'))), + 'tlsport' => array_pop(explode(':', getenv('IMAP_PORT'))), + ), + 'pop3' => array( + 'server' => $mailcow_hostname, + 'port' => array_pop(explode(':', getenv('POPS_PORT'))), + 'tlsport' => array_pop(explode(':', getenv('POP_PORT'))), ), 'smtp' => array( 'server' => $mailcow_hostname, - 'port' => getenv('SMTPS_PORT'), + 'port' => array_pop(explode(':', getenv('SMTPS_PORT'))), + 'tlsport' => array_pop(explode(':', getenv('SUBMISSION_PORT'))), ), 'activesync' => array( - 'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync' + 'url' => 'https://'.$mailcow_hostname.($https_port == 443 ? '' : ':'.$https_port).'/Microsoft-Server-ActiveSync', ), 'caldav' => array( - 'url' => 'https://'.$mailcow_hostname + 'server' => $mailcow_hostname, + 'port' => $https_port, ), 'carddav' => array( - 'url' => 'https://'.$mailcow_hostname - ) + 'server' => $mailcow_hostname, + 'port' => $https_port, + ), ); +unset($https_port); // Where to go after adding and editing objects // Can be "form" or "previous" diff --git a/data/web/mobileconfig.php b/data/web/mobileconfig.php new file mode 100644 index 00000000..198fa4d7 --- /dev/null +++ b/data/web/mobileconfig.php @@ -0,0 +1,167 @@ +prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $email)); + $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; +} + +echo '' . "\n"; +?> + + + + PayloadContent + + + CalDAVAccountDescription + + CalDAVHostName + + CalDAVPort + + CalDAVPrincipalURL + /SOGo/dav/ + CalDAVUseSSL + + CalDAVUsername + + PayloadDescription + Configures CalDAV account. + PayloadDisplayName + CalDAV () + PayloadIdentifier + .CalDAV + PayloadOrganization + + PayloadType + com.apple.caldav.account + PayloadUUID + FC898573-EBA8-48AF-93BD-BFA0C9778FA7 + PayloadVersion + 1 + + + EmailAccountDescription + + EmailAccountType + EmailTypeIMAP + EmailAccountName + + EmailAddress + + IncomingMailServerAuthentication + EmailAuthPassword + IncomingMailServerHostName + + IncomingMailServerPortNumber + + IncomingMailServerUseSSL + + IncomingMailServerUsername + + OutgoingMailServerAuthentication + EmailAuthPassword + OutgoingMailServerHostName + + OutgoingMailServerPortNumber + + OutgoingMailServerUseSSL + + OutgoingMailServerUsername + + OutgoingPasswordSameAsIncomingPassword + + PayloadDescription + Configures email account. + PayloadDisplayName + IMAP Account () + PayloadIdentifier + .email + PayloadOrganization + + PayloadType + com.apple.mail.managed + PayloadUUID + 00294FBB-1016-413E-87B9-652D856D6875 + PayloadVersion + 1 + PreventAppSheet + + PreventMove + + SMIMEEnabled + + + + CardDAVAccountDescription + + CardDAVHostName + + CardDAVPort + + CardDAVPrincipalURL + /SOGo/dav/ + CardDAVUseSSL + + CardDAVUsername + + PayloadDescription + Configures CardDAV accounts + PayloadDisplayName + CardDAV () + PayloadIdentifier + .carddav + PayloadOrganization + + PayloadType + com.apple.carddav.account + PayloadUUID + 0797EF2B-B1F1-4BC7-ABCD-4580862252B4 + PayloadVersion + 1 + + + PayloadDescription + IMAP, CalDAV, CardDAV + PayloadDisplayName + Mailcow + PayloadIdentifier + + PayloadOrganization + + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + 5EE248C5-ACCB-42D8-9199-8F8ED08D5624 + PayloadVersion + 1 + + From e6727b1fd65a3472a4e1894b600d8e4557e6e2fb Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:51:52 +0200 Subject: [PATCH 02/64] [ACME] Iterate alias domains, use hostname in subject field --- data/Dockerfiles/acme/docker-entrypoint.sh | 9 ++++++--- docker-compose.yml | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index b8098a14..f20da206 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -77,9 +77,12 @@ while true; do # Container ids may have changed CONTAINERS_RESTART=($(curl --silent --unix-socket /var/run/docker.sock http/containers/json | jq -rc 'map(select(.Names[] | contains ("nginx-mailcow") or contains ("postfix-mailcow") or contains ("dovecot-mailcow"))) | .[] .Id' | tr "\n" " ")) - while read line; do - SQL_DOMAIN_ARR+=("${line}") + while read domain; do + SQL_DOMAIN_ARR+=("${domain}") done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs) + while read alias_domain; do + SQL_DOMAIN_ARR+=("${alias_domain}") + done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1) @@ -138,7 +141,7 @@ while true; do done # Unique elements - ALL_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${VALIDATED_MAILCOW_HOSTNAME} | xargs -n1 | sort -u | xargs)) + ALL_VALIDATED=($(echo ${VALIDATED_MAILCOW_HOSTNAME} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) if [[ -z ${ALL_VALIDATED[*]} ]]; then echo "Cannot validate hostnames, skipping Let's Encrypt..." exit 0 diff --git a/docker-compose.yml b/docker-compose.yml index e29285ec..68ba9d2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: mysql-mailcow: condition: service_healthy healthcheck: - test: ["CMD", "nslookup", "google.com", "127.0.0.1"] + test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"] interval: 30s timeout: 3s retries: 10 From 4e148c7026e4e73ba6eaf15f9ce4d6c899c16d84 Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:54:29 +0200 Subject: [PATCH 03/64] [Compose] Push Rspamd 1.4; Push acme 1.14; Add Dovecot ulimits --- docker-compose.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 68ba9d2b..04c81a56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,7 @@ services: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.3 + image: mailcow/rspamd:1.4 build: ./data/Dockerfiles/rspamd command: > /bin/bash -c " @@ -186,6 +186,11 @@ services: - "${POPS_PORT:-995}:995" - "${SIEVE_PORT:-4190}:4190" restart: always + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 dns: - 172.22.1.254 dns_search: mailcow-network @@ -277,7 +282,7 @@ services: acme-mailcow: depends_on: - nginx-mailcow - image: mailcow/acme:1.12 + image: mailcow/acme:1.13 build: ./data/Dockerfiles/acme dns: - 172.22.1.254 From 5f5872f78b0e4237c4932f58724ecff7fc6bf7cf Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:54:53 +0200 Subject: [PATCH 04/64] [Rspamd] Initial custom ratelimit support --- data/Dockerfiles/rspamd/Dockerfile | 1 + data/Dockerfiles/rspamd/ratelimit.lua | 717 ++++++++++++++++++++++++++ 2 files changed, 718 insertions(+) create mode 100644 data/Dockerfiles/rspamd/ratelimit.lua diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 408a7a1d..cb5c7d40 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \ && chown _rspamd:_rspamd /run/rspamd COPY settings.conf /etc/rspamd/modules.d/settings.conf +COPY ratelimit.lua /usr/share/rspamd/lua/ratelimit.lua COPY docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/rspamd/ratelimit.lua b/data/Dockerfiles/rspamd/ratelimit.lua new file mode 100644 index 00000000..d9e8f42a --- /dev/null +++ b/data/Dockerfiles/rspamd/ratelimit.lua @@ -0,0 +1,717 @@ +--[[ +Copyright (c) 2011-2015, Vsevolod Stakhov + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +if confighelp then + return +end + +-- A plugin that implements ratelimits using redis or kvstorage server + +local E = {} + +-- Default settings for limits, 1-st member is burst, second is rate and the third is numeric type +local settings = { +} +-- Senders that are considered as bounce +local bounce_senders = {'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon'} +-- Do not check ratelimits for these recipients +local whitelisted_rcpts = {'postmaster', 'mailer-daemon'} +local whitelisted_ip +local whitelisted_user +local max_rcpt = 5 +local redis_params +local ratelimit_symbol +-- Do not delay mail after 1 day +local max_delay = 24 * 3600 +local use_ip_score = false +local rl_prefix = 'rl' +local ip_score_lower_bound = 10 +local ip_score_ham_multiplier = 1.1 +local ip_score_spam_divisor = 1.1 + +local message_func = function(_, limit_type) + return string.format('Ratelimit "%s" exceeded', limit_type) +end + +local rspamd_logger = require "rspamd_logger" +local rspamd_util = require "rspamd_util" +local rspamd_lua_utils = require "lua_util" +local fun = require "fun" + +local user_keywords = {'user'} + +local limit_parser +local function parse_string_limit(lim) + local function parse_time_suffix(s) + if s == 's' then + return 1 + elseif s == 'm' then + return 60 + elseif s == 'h' then + return 3600 + elseif s == 'd' then + return 86400 + end + end + local function parse_num_suffix(s) + if s == '' then + return 1 + elseif s == 'k' then + return 1000 + elseif s == 'm' then + return 1000000 + elseif s == 'g' then + return 1000000000 + end + end + local lpeg = require "lpeg" + + if not limit_parser then + local digit = lpeg.R("09") + limit_parser = {} + limit_parser.integer = + (lpeg.S("+-") ^ -1) * + (digit ^ 1) + limit_parser.fractional = + (lpeg.P(".") ) * + (digit ^ 1) + limit_parser.number = + (limit_parser.integer * + (limit_parser.fractional ^ -1)) + + (lpeg.S("+-") * limit_parser.fractional) + limit_parser.time = lpeg.Cf(lpeg.Cc(1) * + (limit_parser.number / tonumber) * + ((lpeg.S("smhd") / parse_time_suffix) ^ -1), + function (acc, val) return acc * val end) + limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) * + (limit_parser.number / tonumber) * + ((lpeg.S("kmg") / parse_num_suffix) ^ -1), + function (acc, val) return acc * val end) + limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number * + (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) * + limit_parser.time) + end + local t = lpeg.match(limit_parser.limit, lim) + + if t and t[1] and t[2] and t[2] ~= 0 then + return t[1] / t[2], t[1] + end + + rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim) + + return nil +end + +--- Parse atime and bucket of limit +local function parse_limits(data) + local function parse_limit_elt(str) + local elts = rspamd_str_split(str, ':') + if not elts or #elts < 2 then + return {0, 0, 0} + else + local atime = tonumber(elts[1]) + local bucket = tonumber(elts[2]) + local ctime = atime + + if elts[3] then + ctime = tonumber(elts[3]) + end + + if not ctime then + ctime = atime + end + + return {atime,bucket,ctime} + end + end + + return fun.iter(data):map(function(e) + if type(e) == 'string' then + return parse_limit_elt(e) + else + return {0, 0, 0} + end + end):totable() +end + +local function resize_element(x_score, x_total, element) + local x_ip_score + if not x_total then x_total = 0 end + if x_total < ip_score_lower_bound or x_total <= 0 then + x_score = 1 + else + x_score = x_score / x_total + end + if x_score > 0 then + x_ip_score = x_score / ip_score_spam_divisor + element = element * rspamd_util.tanh(2.718281 * x_ip_score) + elseif x_score < 0 then + x_ip_score = ((1 + (x_score * -1)) * ip_score_ham_multiplier) + element = element * x_ip_score + end + return element +end + +--- Check whether this addr is bounce +local function check_bounce(from) + return fun.any(function(b) return b == from end, bounce_senders) +end + +local custom_keywords = {} + +local keywords = { + ['ip'] = { + ['get_value'] = function(task) + local ip = task:get_ip() + if ip and ip:is_valid() then return ip end + return nil + end, + }, + ['rip'] = { + ['get_value'] = function(task) + local ip = task:get_ip() + if ip and ip:is_valid() and not ip:is_local() then return ip end + return nil + end, + }, + ['from'] = { + ['get_value'] = function(task) + local from = task:get_from(0) + if ((from or E)[1] or E).addr then + return from[1]['addr'] + end + return nil + end, + }, + ['bounce'] = { + ['get_value'] = function(task) + local from = task:get_from(0) + if not ((from or E)[1] or E).user then + return '_' + end + if check_bounce(from[1]['user']) then return '_' else return nil end + end, + }, + ['asn'] = { + ['get_value'] = function(task) + local asn = task:get_mempool():get_variable('asn') + if not asn then + return nil + else + return asn + end + end, + }, + ['user'] = { + ['get_value'] = function(task) + local auser = task:get_user() + if not auser then + return nil + else + return auser + end + end, + }, + ['to'] = { + ['get_value'] = function() + return '%s' -- 'to' is special + end, + }, +} + +local function dynamic_rate_key(task, rtype) + local key_t = {rl_prefix, rtype} + local key_keywords = rspamd_str_split(rtype, '_') + local have_to, have_user = false, false + for _, v in ipairs(key_keywords) do + if (custom_keywords[v] and type(custom_keywords[v]['condition']) == 'function') then + if not custom_keywords[v]['condition']() then return nil end + end + local ret + if custom_keywords[v] and type(custom_keywords[v]['get_value']) == 'function' then + ret = custom_keywords[v]['get_value'](task) + elseif keywords[v] and type(keywords[v]['get_value']) == 'function' then + ret = keywords[v]['get_value'](task) + end + if not ret then return nil end + for _, uk in ipairs(user_keywords) do + if v == uk then have_user = true end + if have_user then break end + end + if v == 'to' then have_to = true end + if type(ret) ~= 'string' then ret = tostring(ret) end + table.insert(key_t, ret) + end + if (not have_user) and task:get_user() then + return nil + end + if not have_to then + return table.concat(key_t, ":") + else + local rate_keys = {} + local rcpts = task:get_recipients(0) + if not ((rcpts or E)[1] or E).addr then + return nil + end + local key_s = table.concat(key_t, ":") + local total_rcpt = 0 + for _, r in ipairs(rcpts) do + if r['addr'] and total_rcpt < max_rcpt then + local key_f = string.format(key_s, r['addr']) + table.insert(rate_keys, key_f) + total_rcpt = total_rcpt + 1 + end + end + return rate_keys + end +end + +--- Check specific limit inside redis +local function check_limits(task, args) + + local key = fun.foldl(function(acc, k) return acc .. k[2] end, '', args) + local ret + --- Called when value is got from server + local function rate_get_cb(err, data) + if err then + rspamd_logger.infox(task, 'got error while getting limit: %1', err) + end + if not data then return end + local ntime = rspamd_util.get_time() + local asn_score,total_asn, + country_score,total_country, + ipnet_score,total_ipnet, + ip_score, total_ip + if use_ip_score then + asn_score,total_asn, + country_score,total_country, + ipnet_score,total_ipnet, + ip_score, total_ip = task:get_mempool():get_variable('ip_score', + 'double,double,double,double,double,double,double,double') + end + + fun.each(function(elt, limit, rtype) + local bucket = elt[2] + local rate = limit[2] + local threshold = limit[1] + local atime = elt[1] + local ctime = elt[3] + + if atime == 0 then return end + + if use_ip_score then + local key_keywords = rspamd_str_split(rtype, '_') + local has_asn, has_ip = false, false + for _, v in ipairs(key_keywords) do + if v == "asn" then has_asn = true end + if v == "ip" then has_ip = true end + if has_ip and has_asn then break end + end + if has_asn and not has_ip then + bucket = resize_element(asn_score, total_asn, bucket) + rate = resize_element(asn_score, total_asn, rate) + elseif has_ip then + if total_ip and total_ip > ip_score_lower_bound then + bucket = resize_element(ip_score, total_ip, bucket) + rate = resize_element(ip_score, total_ip, rate) + elseif total_ipnet and total_ipnet > ip_score_lower_bound then + bucket = resize_element(ipnet_score, total_ipnet, bucket) + rate = resize_element(ipnet_score, total_ipnet, rate) + elseif total_asn and total_asn > ip_score_lower_bound then + bucket = resize_element(asn_score, total_asn, bucket) + rate = resize_element(asn_score, total_asn, rate) + elseif total_country and total_country > ip_score_lower_bound then + bucket = resize_element(country_score, total_country, bucket) + rate = resize_element(country_score, total_country, rate) + else + bucket = resize_element(ip_score, total_ip, bucket) + rate = resize_element(ip_score, total_ip, rate) + end + end + end + + if atime - ctime > max_delay then + rspamd_logger.infox(task, 'limit is too old: %1 seconds; ignore it', + atime - ctime) + else + bucket = bucket - rate * (ntime - atime); + if bucket > 0 then + if ratelimit_symbol then + local mult = 2 * rspamd_util.tanh(bucket / (threshold * 2)) + + if mult > 0.5 then + task:insert_result(ratelimit_symbol, mult, + rtype .. ':' .. string.format('%.2f', mult)) + end + else + if bucket > threshold then + rspamd_logger.infox(task, + 'ratelimit "%s" exceeded: %s elements with %s limit', + rtype, bucket, threshold) + task:set_pre_result('soft reject', + message_func(task, rtype, bucket, threshold)) + end + end + end + end + end, fun.zip(parse_limits(data), fun.map(function(a) return a[1] end, args), + fun.map(function(a) return rspamd_str_split(a[2], ":")[2] end, args))) + end + + ret = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + rate_get_cb, --callback + 'mget', -- command + fun.totable(fun.map(function(l) return l[2] end, args)) -- arguments + ) + if not ret then + rspamd_logger.errx(task, 'got error connecting to redis') + end +end + +--- Set specific limit inside redis +local function set_limits(task, args) + local key = fun.foldl(function(acc, k) return acc .. k[2] end, '', args) + local ret, upstream + + local function rate_set_cb(err) + if err then + rspamd_logger.infox(task, 'got error %s when setting ratelimit record on server %s', + err, upstream:get_addr()) + end + end + local function rate_get_cb(err, data) + if err then + rspamd_logger.infox(task, 'got error while setting limit: %1', err) + end + if not data then return end + local ntime = rspamd_util.get_time() + local values = {} + fun.each(function(elt, limit) + local bucket = elt[2] + local rate = limit[1][2] + local atime = elt[1] + local ctime = elt[3] + + if atime - ctime > max_delay then + rspamd_logger.infox(task, 'limit is too old: %1 seconds; start it over', + atime - ctime) + bucket = 1 + ctime = ntime + else + if bucket > 0 then + bucket = bucket - rate * (ntime - atime) + 1; + if bucket < 0 then + bucket = 1 + end + else + bucket = 1 + end + end + + if ctime == 0 then ctime = ntime end + + local lstr = string.format('%.3f:%.3f:%.3f', ntime, bucket, ctime) + table.insert(values, {limit[2], max_delay, lstr}) + end, fun.zip(parse_limits(data), fun.iter(args))) + + if #values > 0 then + local conn + ret,conn,upstream = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + true, -- is write + rate_set_cb, --callback + 'setex', -- command + values[1] -- arguments + ) + + if conn then + fun.each(function(v) + conn:add_cmd('setex', v) + end, fun.drop_n(1, values)) + else + rspamd_logger.errx(task, 'got error while connecting to redis') + end + end + end + + local _ + ret,_,upstream = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + rate_get_cb, --callback + 'mget', -- command + fun.totable(fun.map(function(l) return l[2] end, args)) -- arguments + ) + if not ret then + rspamd_logger.errx(task, 'got error connecting to redis') + end +end + +--- Check or update ratelimit +local function rate_test_set(task, func) + local args = {} + -- Get initial task data + local ip = task:get_from_ip() + if ip and ip:is_valid() and whitelisted_ip then + if whitelisted_ip:get_key(ip) then + -- Do not check whitelisted ip + rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP') + return + end + end + -- Parse all rcpts + local rcpts = task:get_recipients() + local rcpts_user = {} + if rcpts then + fun.each(function(r) table.insert(rcpts_user, r['user']) end, rcpts) + if fun.any(function(r) + fun.any(function(w) return r == w end, whitelisted_rcpts) end, + rcpts_user) then + + rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient') + return + end + end + -- Get user (authuser) + if whitelisted_user then + local auser = task:get_user() + if whitelisted_user:get_key(auser) then + rspamd_logger.infox(task, 'skip ratelimit for whitelisted user') + return + end + end + + local rate_key + for k in pairs(settings) do + rate_key = dynamic_rate_key(task, k) + if rate_key then + if type(rate_key) == 'table' then + for _, rk in ipairs(rate_key) do + if type(settings[k]) == 'table' then + table.insert(args, {settings[k], rk}) + elseif type(settings[k]) == 'string' and + (custom_keywords[settings[k]] and type(custom_keywords[settings[k]]['get_limit']) == 'function') then + local res = custom_keywords[settings[k]]['get_limit'](task) + if type(res) == 'table' then + table.insert(args, {res, rate_key}) + elseif type(res) == 'string' then + local plim, size = parse_string_limit(res) + if plim then + table.insert(args, {{size, plim, 1}, rate_key}) + end + end + end + end + else + if type(settings[k]) == 'table' then + table.insert(args, {settings[k], rate_key}) + elseif type(settings[k]) == 'string' and + (custom_keywords[settings[k]] and type(custom_keywords[settings[k]]['get_limit']) == 'function') then + local res = custom_keywords[settings[k]]['get_limit'](task) + if type(res) == 'table' then + table.insert(args, {res, rate_key}) + elseif type(res) == 'string' then + local plim, size = parse_string_limit(res) + if plim then + table.insert(args, {{size, plim, 1}, rate_key}) + end + end + end + end + end + end + + if #args > 0 then + func(task, args) + end +end + +--- Check limit +local function rate_test(task) + if rspamd_lua_utils.is_rspamc_or_controller(task) then return end + rate_test_set(task, check_limits) +end +--- Update limit +local function rate_set(task) + local action = task:get_metric_action('default') + + if action ~= 'soft reject' then + if rspamd_lua_utils.is_rspamc_or_controller(task) then return end + rate_test_set(task, set_limits) + end +end + + +--- Parse a single limit description +local function parse_limit(str) + local params = rspamd_str_split(str, ':') + + local function set_limit(limit, burst, rate) + limit[1] = tonumber(burst) + limit[2] = tonumber(rate) + end + + if #params ~= 3 then + rspamd_logger.errx(rspamd_config, 'invalid limit definition: ' .. str) + return + end + + local key_keywords = rspamd_str_split(params[1], '_') + for _, k in ipairs(key_keywords) do + if (custom_keywords[k] and type(custom_keywords[k]['get_value']) == 'function') or + (keywords[k] and type(keywords[k]['get_value']) == 'function') then + set_limit(settings[params[1]], params[2], params[3]) + else + rspamd_logger.errx(rspamd_config, 'invalid limit type: ' .. params[1]) + end + end +end + +local opts = rspamd_config:get_all_opt('ratelimit') +if opts then + local rates = opts['limit'] + if rates and type(rates) == 'table' then + fun.each(parse_limit, rates) + elseif rates and type(rates) == 'string' then + parse_limit(rates) + end + + if opts['rates'] and type(opts['rates']) == 'table' then + -- new way of setting limits + fun.each(function(t, lim) + if type(lim) == 'table' then + settings[t] = lim + elseif type(lim) == 'string' then + local plim, size = parse_string_limit(lim) + if plim then + settings[t] = {size, plim, 1} + end + end + end, opts['rates']) + end + + if opts['dynamic_rates'] and type(opts['dynamic_rates']) == 'table' then + fun.each(function(t, lim) + if type(lim) == 'string' then + settings[t] = lim + end + end, opts['dynamic_rates']) + end + + local enabled_limits = fun.totable(fun.map(function(t) + return t + end, fun.filter(function(_, lim) + return type(lim) == 'string' or + (type(lim) == 'table' and type(lim[1]) == 'number' and lim[1] > 0) + or (type(lim) == 'table' and (lim[3])) + end, settings))) + rspamd_logger.infox(rspamd_config, 'enabled rate buckets: [%1]', table.concat(enabled_limits, ',')) + + if opts['whitelisted_rcpts'] and type(opts['whitelisted_rcpts']) == 'string' then + whitelisted_rcpts = rspamd_str_split(opts['whitelisted_rcpts'], ',') + elseif type(opts['whitelisted_rcpts']) == 'table' then + whitelisted_rcpts = opts['whitelisted_rcpts'] + end + + if opts['whitelisted_ip'] then + whitelisted_ip = rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix', + 'Ratelimit whitelist ip map') + end + + if opts['whitelisted_user'] then + whitelisted_user = rspamd_map_add('ratelimit', 'whitelisted_user', 'set', + 'Ratelimit whitelist user map') + end + + if opts['symbol'] then + -- We want symbol instead of pre-result + ratelimit_symbol = opts['symbol'] + end + + if opts['max_rcpt'] then + max_rcpt = tonumber(opts['max_rcpt']) + end + + if opts['max_delay'] then + max_rcpt = tonumber(opts['max_delay']) + end + + if opts['use_ip_score'] then + use_ip_score = true + local ip_score_opts = rspamd_config:get_all_opt('ip_score') + if ip_score_opts and ip_score_opts['lower_bound'] then + ip_score_lower_bound = ip_score_opts['lower_bound'] + end + end + + if opts['custom_keywords'] then + custom_keywords = dofile(opts['custom_keywords']) + end + + if opts['user_keywords'] then + user_keywords = opts['user_keywords'] + end + + if opts['message_func'] then + message_func = assert(load(opts['message_func']))() + end + + redis_params = rspamd_parse_redis_server('ratelimit') + if not redis_params then + rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module') + else + if not ratelimit_symbol and not use_ip_score then + rspamd_config:register_symbol({ + name = 'RATELIMIT_CHECK', + callback = rate_test, + type = 'prefilter', + priority = 4, + }) + else + local symbol + if not ratelimit_symbol then + symbol = 'RATELIMIT_CHECK' + else + symbol = ratelimit_symbol + end + local id = rspamd_config:register_symbol({ + name = symbol, + callback = rate_test, + }) + if use_ip_score then + rspamd_config:register_dependency(id, 'IP_SCORE') + end + end + rspamd_config:register_symbol({ + name = 'RATELIMIT_SET', + type = 'postfilter', + priority = 5, + callback = rate_set, + }) + for _, v in pairs(custom_keywords) do + if type(v) == 'table' and type(v['init']) == 'function' then + v['init']() + end + end + end +end + + From 256c9d86ddd6ea1caf2f4dc5dc252bf07bc9de0a Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:55:14 +0200 Subject: [PATCH 05/64] [Rspamd] Initial custom ratelimit support --- data/conf/rspamd/local.d/ratelimit.conf | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 data/conf/rspamd/local.d/ratelimit.conf diff --git a/data/conf/rspamd/local.d/ratelimit.conf b/data/conf/rspamd/local.d/ratelimit.conf deleted file mode 100644 index eca11a12..00000000 --- a/data/conf/rspamd/local.d/ratelimit.conf +++ /dev/null @@ -1,18 +0,0 @@ -rates { - # Limit for all mail per recipient (burst 100, rate 2 per minute) - to = [100, 0.033333333]; - # Limit for all mail per one source ip (burst 30, rate 1.5 per minute) - to_ip = [30, 0.025]; - # Limit for all mail per one source ip and from address (burst 20, rate 1 per minute) - to_ip_from = [20, 0.01666666667]; - # Limit for all bounce mail (burst 10, rate 2 per hour) - bounce_to = [10, 0.000555556]; - # Limit for bounce mail per one source ip (burst 5, rate 1 per hour) - bounce_to_ip = [5, 0.000277778]; - # Limit for all mail per authenticated user (burst 20, rate 1 per minute) - user = [20, 0.01666666667]; -} -# If symbol is specified, then it is inserted instead of setting result -#symbol = "R_RATELIMIT"; -whitelisted_rcpts = "postmaster,mailer-daemon"; -max_rcpt = 5; From 84ad579437d6e978923b4b82718867fdeb8c9b77 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sun, 16 Jul 2017 11:03:28 +0200 Subject: [PATCH 06/64] [Web] Initial ratelimit support, more API actions --- data/web/admin.php | 4 +- data/web/css/edit.css | 3 + data/web/edit.php | 85 ++- data/web/inc/functions.domain_admin.inc.php | 507 +++++++++++++++++ data/web/inc/functions.inc.php | 573 +------------------- data/web/inc/functions.mailbox.inc.php | 134 ++++- data/web/inc/prerequisites.inc.php | 1 + data/web/inc/sessions.inc.php | 2 + data/web/inc/triggers.inc.php | 41 -- data/web/js/api.js | 20 +- data/web/json_api.php | 181 +++++-- data/web/user.php | 4 +- 12 files changed, 855 insertions(+), 700 deletions(-) create mode 100644 data/web/inc/functions.domain_admin.inc.php diff --git a/data/web/admin.php b/data/web/admin.php index 0e0003a8..284bf31b 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -56,7 +56,7 @@ $tfa_data = get_tfa();
- +
@@ -253,7 +253,7 @@ XYZ - +
diff --git a/data/web/css/edit.css b/data/web/css/edit.css index fe4d9fff..07d4e745 100644 --- a/data/web/css/edit.css +++ b/data/web/css/edit.css @@ -27,3 +27,6 @@ table.footable>tbody>tr.footable-empty>td { user-select: none; padding:10px 0 10px 0; } +.inputMissingAttr { + border-color: #FF4136; +} diff --git a/data/web/edit.php b/data/web/edit.php index d156dba0..1f250535 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -25,9 +25,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm ?>


-
"> + -
@@ -43,7 +42,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -55,20 +54,19 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm


-
"> + -
@@ -122,7 +120,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -139,14 +137,14 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm !empty($_GET["domain"])) { $domain = $_GET["domain"]; $result = mailbox('get', 'domain_details', $domain); + $rl = mailbox('get', 'domain_ratelimit', $domain); if (!empty($result)) { ?>

-
"> + -
@@ -203,7 +201,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -223,6 +221,23 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm } ?>
+
+
+ + +
+
+ +
+
+ +
+
+

@@ -282,12 +297,12 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm !empty($_GET["aliasdomain"])) { $alias_domain = $_GET["aliasdomain"]; $result = mailbox('get', 'alias_domain_details', $alias_domain); + $rl = mailbox('get', 'domain_ratelimit', $alias_domain); if (!empty($result)) { ?>

-
"> + -
@@ -303,10 +318,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
+
+
+
+ + +
+
+ +
+
+ +
+
@@ -334,10 +366,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if (!empty($result)) { ?>

-
"> + -
@@ -355,7 +386,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
-
- +
@@ -439,10 +470,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if (!empty($result)) { ?>

-
"> + -
@@ -475,7 +505,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -501,11 +531,10 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == if (!empty($result)) { ?>

-
"> + -
@@ -587,7 +616,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
- +
diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php new file mode 100644 index 00000000..aa434079 --- /dev/null +++ b/data/web/inc/functions.domain_admin.inc.php @@ -0,0 +1,507 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + if (empty($domains)) { + $_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 (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + if ($password != $password2) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + $password_hashed = hash_password($password); + foreach ($domains 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) { + domain_admin('delete', $username); + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + try { + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); + $stmt->execute(array( + ':username' => $username, + ':password_hashed' => $password_hashed, + ':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)) + ); + break; + case 'edit': + 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") { + if (!is_array($_data['username'])) { + $usernames = array(); + $usernames[] = $_data['username']; + } + else { + $usernames = $_data['username']; + } + foreach ($usernames as $username) { + $is_now = domain_admin('details', $username); + $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; + $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $password = $_data['password']; + $password2 = $_data['password2']; + + if (!empty($domains)) { + foreach ($domains 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_new))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + if ($username_new != $username) { + if (!empty(domain_admin('details', $username_new)['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 (!empty($domains)) { + foreach ($domains as $domain) { + try { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username_new, :domain, :created, :active)"); + $stmt->execute(array( + ':username_new' => $username_new, + ':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 (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + 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` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + else { + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + 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(implode(', ', $usernames))) + ); + } + // Domain administrator + // Can only edit itself + elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { + $username = $_SESSION['mailcow_cc_username']; + $password_old = $_data['user_old_pass']; + $password_new = $_data['user_new_pass']; + $password_new2 = $_data['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 (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $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 `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':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)) + ); + } + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $usernames = (array)$_data['username']; + foreach ($usernames as $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(implode(', ', $usernames))) + ); + break; + case 'get': + $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; + break; + case 'details': + $domainadmindata = array(); + + if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { + return false; + } + elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { + return false; + } + try { + $stmt = $pdo->prepare("SELECT + `tfa`.`active` AS `tfa_active_int`, + CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, + `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' => $_data + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = $row['tfa_active']; + $domainadmindata['active'] = $row['active']; + $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $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' => $_data)); + $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' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['unselected_domains'][] = $row['domain']; + } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $domainadmindata; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index b3420530..66dad50f 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -73,7 +73,6 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { unset($lines[0]); return base64_decode(implode('', $lines)); } - if (empty($starttls)) { $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); $stream = stream_socket_client('tls://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context); @@ -113,7 +112,6 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT); stream_set_blocking($stream, false); } - $params = stream_context_get_params($stream); if (!empty($params['options']['ssl']['peer_certificate'])) { $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']); @@ -142,30 +140,6 @@ function verify_ssha256($hash, $password) { return false; } } -function doveadm_authenticate($hash, $algorithm, $password) { - $descr = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); - $pipes = array(); - $process = proc_open("/usr/bin/doveadm pw -s ".$algorithm." -t '".$hash."'", $descr, $pipes); - if (is_resource($process)) { - fputs($pipes[0], $password); - fclose($pipes[0]); - while ($f = fgets($pipes[1])) { - if (preg_match('/(verified)/', $f)) { - proc_close($process); - return true; - } - return false; - } - fclose($pipes[1]); - while ($f = fgets($pipes[2])) { - proc_close($process); - return false; - } - fclose($pipes[2]); - proc_close($process); - } - return false; -} function check_login($user, $pass) { global $pdo; global $redis; @@ -272,7 +246,6 @@ function edit_admin_account($postarray) { ); return false; } - if (!empty($password) && !empty($password2)) { if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { $_SESSION['return'] = array( @@ -348,28 +321,20 @@ function edit_admin_account($postarray) { function edit_user_account($postarray) { global $lang; global $pdo; - if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - else { - $username = $postarray['username']; - } + $username = $_SESSION['mailcow_cc_username']; + $role = $_SESSION['mailcow_cc_role']; + $password_old = $postarray['user_old_pass']; + if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; } - else { - $username = $_SESSION['mailcow_cc_username']; - } - $password_old = $postarray['user_old_pass']; - if (isset($postarray['user_new_pass']) && isset($postarray['user_new_pass2'])) { $password_new = $postarray['user_new_pass']; $password_new2 = $postarray['user_new_pass2']; } - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `username` = :user"); @@ -382,7 +347,6 @@ function edit_user_account($postarray) { ); return false; } - if (isset($password_new) && isset($password_new2)) { if (!empty($password_new2) && !empty($password_new)) { if ($password_new2 != $password_new) { @@ -486,293 +450,12 @@ function is_valid_domain_name($domain_name) { && preg_match("/^.{1,253}$/", $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']; - $domains = (array)$postarray['domains']; - $active = intval($postarray['active']); - - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - if (empty($domains)) { - $_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 (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - if ($password != $password2) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_mismatch']) - ); - return false; - } - $password_hashed = hash_password($password); - foreach ($domains 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`, `active`) - VALUES (:username, :password_hashed, '0', :active)"); - $stmt->execute(array( - ':username' => $username, - ':password_hashed' => $password_hashed, - ':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; - } - $usernames = (array)$postarray['username']; - foreach ($usernames as $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(implode(', ', $usernames))) - ); -} -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") { - return false; - } - if (!isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "domainadmin") { - return false; - } - (!isset($domain_admin)) ? $domain_admin = $_SESSION['mailcow_cc_username'] : null; - - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $domain_admin))) { - return false; - } - try { - $stmt = $pdo->prepare("SELECT - `tfa`.`active` AS `tfa_active_int`, - CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, - `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); - if (empty($row)) { - return false; - } - $domainadmindata['username'] = $row['username']; - $domainadmindata['tfa_active'] = $row['tfa_active']; - $domainadmindata['active'] = $row['active']; - $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; - $domainadmindata['active_int'] = $row['active_int']; - $domainadmindata['modified'] = $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']; - } - if (!isset($domainadmindata['unselected_domains'])) { - $domainadmindata['unselected_domains'] = ""; - } - } - 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; global $tfa; - if ($_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['mailcow_cc_role'] != "admin") { $_SESSION['return'] = array( @@ -847,7 +530,6 @@ function set_tfa($postarray) { 'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars($username)) ); break; - case "u2f": $key_id = (!isset($postarray["key_id"])) ? 'unidentified' : $postarray["key_id"]; try { @@ -871,7 +553,6 @@ function set_tfa($postarray) { return false; } break; - case "totp": $key_id = (!isset($postarray["key_id"])) ? 'unidentified' : $postarray["key_id"]; if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) { @@ -900,7 +581,6 @@ function set_tfa($postarray) { ); } break; - case "none": try { $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); @@ -977,7 +657,6 @@ function get_tfa($username = null) { elseif (empty($username)) { return false; } - $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); @@ -1041,7 +720,6 @@ function verify_tfa_login($username, $token) { global $yubi; global $u2f; global $tfa; - $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); @@ -1126,237 +804,6 @@ function verify_tfa_login($username, $token) { } 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") { - if (!is_array($postarray['username'])) { - $usernames = array(); - $usernames[] = $postarray['username']; - } - else { - $usernames = $postarray['username']; - } - foreach ($usernames as $username) { - $is_now = get_domain_admin_details($username); - $domains = (isset($postarray['domains'])) ? (array)$postarray['domains'] : null; - if (!empty($is_now)) { - $active = (isset($postarray['active'])) ? $postarray['active'] : $is_now['active_int']; - $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; - $username_new = (!empty($postarray['username_new'])) ? $postarray['username_new'] : $is_now['username']; - } - else { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - $password = $postarray['password']; - $password2 = $postarray['password2']; - - if (!empty($domains)) { - foreach ($domains 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_new))) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - if ($username_new != $username) { - if (!empty(get_domain_admin_details($username_new)['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 (!empty($domains)) { - foreach ($domains as $domain) { - try { - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username_new, :domain, :created, :active)"); - $stmt->execute(array( - ':username_new' => $username_new, - ':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 (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - 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` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($postarray['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - else { - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($postarray['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - 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(implode(', ', $usernames))) - ); - } - // 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 (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $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 `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':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; @@ -1438,4 +885,4 @@ function get_logs($container, $lines = 100) { } return false; } -?> +?> \ No newline at end of file diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 597d9d87..de8bf0ac 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -879,7 +879,7 @@ function mailbox($_action, $_type, $_data = null) { $alias_domain = idn_to_ascii(strtolower(trim($alias_domain))); $is_now = mailbox('get', 'alias_domain_details', $alias_domain); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; $target_domain = (!empty($_data['target_domain'])) ? idn_to_ascii(strtolower(trim($_data['target_domain']))) : $is_now['target_domain']; } else { @@ -903,7 +903,7 @@ function mailbox($_action, $_type, $_data = null) { ); return false; } - if (empty(mailbox('get', 'domain_details', $target_domain))) { + if (empty(mailbox('get', 'domain_details', $target_domain)) || !empty(mailbox('get', 'alias_domain_details', $target_domain))) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['target_domain_invalid']) @@ -950,12 +950,10 @@ function mailbox($_action, $_type, $_data = null) { ); return false; } - $tls_enforce_out = intval($_data['tls_enforce_out']); - $tls_enforce_in = intval($_data['tls_enforce_in']); $is_now = mailbox('get', 'tls_policy', $username); if (!empty($is_now)) { - $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? $_data['tls_enforce_in'] : $is_now['tls_enforce_in']; - $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? $_data['tls_enforce_out'] : $is_now['tls_enforce_out']; + $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in']; + $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out']; } else { $_SESSION['return'] = array( @@ -1136,6 +1134,63 @@ function mailbox($_action, $_type, $_data = null) { 'msg' => sprintf($lang['success']['mailbox_modified'], implode(', ', $usernames)) ); break; + case 'domain_ratelimit': + $rl_value = intval($_data['rl_value']); + $rl_frame = $_data['rl_frame']; + if (!in_array($rl_frame, array('s', 'm', 'h'))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Ratelimit time frame is incorrect' + ); + return false; + } + if (!is_array($_data['domain'])) { + $domains = array(); + $domains[] = $_data['domain']; + } + else { + $domains = $_data['domain']; + } + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain) || !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($rl_value)) { + try { + $redis->hDel('RL_OBJECT', $domain); + $redis->hDel('RL_VALUE', $domain); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + } + else { + try { + $redis->hSet('RL_OBJECT', $domain, '1'); + $redis->hSet('RL_VALUE', $domain, $rl_value . ' / 1' . $rl_frame); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_modified'], implode(', ', $domains)) + ); + break; case 'syncjob': if (!is_array($_data['id'])) { $ids = array(); @@ -1149,9 +1204,9 @@ function mailbox($_action, $_type, $_data = null) { if (!empty($is_now)) { $username = $is_now['user2']; $user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1']; - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; - $delete2duplicates = (isset($_data['delete2duplicates'])) ? $_data['delete2duplicates'] : $is_now['delete2duplicates']; - $delete1 = (isset($_data['delete1'])) ? $_data['delete1'] : $is_now['delete1']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates']; + $delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1']; $port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1']; $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1']; $host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1']; @@ -1253,7 +1308,7 @@ function mailbox($_action, $_type, $_data = null) { foreach ($addresses as $address) { $is_now = mailbox('get', 'alias_details', $address); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; $goto = (!empty($_data['goto'])) ? $_data['goto'] : $is_now['goto']; } else { @@ -1383,9 +1438,9 @@ function mailbox($_action, $_type, $_data = null) { elseif ($_SESSION['mailcow_cc_role'] == "admin") { $is_now = mailbox('get', 'domain_details', $domain); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; - $backupmx = (isset($_data['backupmx'])) ? $_data['backupmx'] : $is_now['backupmx_int']; - $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? $_data['relay_all_recipients'] : $is_now['relay_all_recipients_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $is_now['backupmx_int']; + $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $is_now['relay_all_recipients_int']; $aliases = (!empty($_data['aliases'])) ? $_data['aliases'] : $is_now['max_num_aliases_for_domain']; $mailboxes = (!empty($_data['mailboxes'])) ? $_data['mailboxes'] : $is_now['max_num_mboxes_for_domain']; $maxquota = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576); @@ -1524,7 +1579,7 @@ function mailbox($_action, $_type, $_data = null) { } $is_now = mailbox('get', 'mailbox_details', $username); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; $name = (!empty($_data['name'])) ? $_data['name'] : $is_now['name']; $domain = $is_now['domain']; $quota_m = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['quota'] / 1048576); @@ -1588,19 +1643,15 @@ function mailbox($_action, $_type, $_data = null) { mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro'] ); // Get sender_acl items from POST array - $sender_acl_domain_admin = ($_data['sender_acl'] == "0") ? array() : $_data['sender_acl']; + $sender_acl_domain_admin = ($_data['sender_acl'] == "0") ? array() : (array)$_data['sender_acl']; 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; + // Check items in POST array and skip invalid + foreach ($sender_acl_domain_admin as $key => $val) { + if (!filter_var($val, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($val, '@'))) { + unset($sender_acl_domain_admin[$key]); } - if (is_valid_domain_name(ltrim($sender_acl, '@'))) { - if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($sender_acl, '@'))) { + if (is_valid_domain_name(ltrim($val, '@'))) { + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($val, '@'))) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['sender_acl_invalid']) @@ -1608,8 +1659,8 @@ function mailbox($_action, $_type, $_data = null) { return false; } } - if (filter_var($sender_acl, FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $sender_acl)) { + if (filter_var($val, FILTER_VALIDATE_EMAIL)) { + if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['sender_acl_invalid']) @@ -1761,8 +1812,8 @@ function mailbox($_action, $_type, $_data = null) { foreach ($names as $name) { $is_now = mailbox('get', 'resource_details', $name); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; - $multiple_bookings = (isset($_data['multiple_bookings'])) ? $_data['multiple_bookings'] : $is_now['multiple_bookings_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $multiple_bookings = (isset($_data['multiple_bookings'])) ? intval($_data['multiple_bookings']) : $is_now['multiple_bookings_int']; $description = (!empty($_data['description'])) ? $_data['description'] : $is_now['description']; $kind = (!empty($_data['kind'])) ? $_data['kind'] : $is_now['kind']; } @@ -2267,6 +2318,31 @@ function mailbox($_action, $_type, $_data = null) { } return $aliases; break; + case 'domain_ratelimit': + $aliases = array(); + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { + return false; + } + try { + if (($rl_value = $redis->hGet('RL_VALUE', $_data)) && $redis->hGet('RL_OBJECT', $_data)) { + $rl = explode(' / 1', $rl_value); + $data['value'] = $rl[0]; + $data['frame'] = $rl[1]; + return $data; + } + else { + return false; + } + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + return false; + break; case 'alias_details': $aliasdata = array(); try { diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index e00fd31a..ebc453f8 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -61,6 +61,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php'; include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php'; diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index e2919f7a..d23d2cba 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -1,6 +1,8 @@ diff --git a/data/web/js/api.js b/data/web/js/api.js index ec26709a..1fd962fb 100644 --- a/data/web/js/api.js +++ b/data/web/js/api.js @@ -64,8 +64,23 @@ $(document).ready(function() { // If clicked element #edit_selected is in a form with the same data-id as the button, // we merge all input fields by {"name":"value"} into api-attr if ($(this).closest("form").data('id') == id) { - var attr_to_merge = $(this).closest("form").serializeObject(); - var api_attr = $.extend(api_attr, attr_to_merge) + var req_empty = false; + $(this).closest("form").find('select, textarea, input').each(function() { + if ($(this).prop('required')) { + if (!$(this).val()) { + req_empty = true; + $(this).addClass('inputMissingAttr'); + } else { + $(this).removeClass('inputMissingAttr'); + } + } + }); + if (!req_empty) { + var attr_to_merge = $(this).closest("form").serializeObject(); + var api_attr = $.extend(api_attr, attr_to_merge) + } else { + return false; + } } // If clicked element #edit_selected has data-item attribute, it is added to "items" if (typeof $(this).data('item') !== 'undefined') { @@ -77,6 +92,7 @@ $(document).ready(function() { } if (typeof multi_data[id] == "undefined") return; api_items = multi_data[id]; + // alert(JSON.stringify(api_attr)); if (Object.keys(api_items).length !== 0) { $.ajax({ type: "POST", diff --git a/data/web/json_api.php b/data/web/json_api.php index e4086520..56d4e741 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -426,7 +426,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "domain-admin": if (isset($_POST['attr'])) { $attr = (array)json_decode($_POST['attr'], true); - if (add_domain_admin($attr) === false) { + if (domain_admin('add', $attr) === false) { if (isset($_SESSION['return'])) { echo json_encode($_SESSION['return']); } @@ -826,10 +826,10 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "domain-admin": switch ($object) { case "all": - $domain_admins = get_domain_admins(); + $domain_admins = domain_admin('get'); if (!empty($domain_admins)) { foreach ($domain_admins as $domain_admin) { - if ($details = get_domain_admin_details($domain_admin)) { + if ($details = domain_admin('details', $domain_admin)) { $data[] = $details; } else { @@ -849,7 +849,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u break; default: - $data = get_domain_admin_details($object); + $data = domain_admin('details', $object); if (!isset($data) || empty($data)) { echo '{}'; } @@ -1385,7 +1385,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u if (isset($_POST['items'])) { $items = (array)json_decode($_POST['items'], true); if (is_array($items)) { - if (delete_domain_admin(array('username' => $items)) === false) { + if (domain_admin('delete', array('username' => $items)) === false) { if (isset($_SESSION['return'])) { echo json_encode($_SESSION['return']); } @@ -1603,6 +1603,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u } break; case "mailbox": + // sender_acl:0 removes all entries if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); $attr = (array)json_decode($_POST['attr'], true); @@ -1778,6 +1779,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "domain-ratelimit": + if (isset($_POST['items']) && isset($_POST['attr'])) { + $items = (array)json_decode($_POST['items'], true); + $attr = (array)json_decode($_POST['attr'], true); + $postarray = array_merge(array('domain' => $items), $attr); + if (is_array($postarray['domain'])) { + if (mailbox('edit', 'domain_ratelimit', $postarray) === 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' + )); + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } + break; case "alias-domain": if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); @@ -1822,7 +1867,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; - case "spam_score": + case "spam-score": if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); $attr = (array)json_decode($_POST['attr'], true); @@ -1872,7 +1917,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u $attr = (array)json_decode($_POST['attr'], true); $postarray = array_merge(array('username' => $items), $attr); if (is_array($postarray['username'])) { - if (edit_domain_admin($postarray) === false) { + if (domain_admin('edit', $postarray) === false) { if (isset($_SESSION['return'])) { echo json_encode($_SESSION['return']); } @@ -1989,39 +2034,109 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; - case "admin": - // No items as there is only one admin - if (isset($_POST['attr'])) { - $attr = (array)json_decode($_POST['attr'], true); - if (edit_admin_account($attr) === false) { - if (isset($_SESSION['return'])) { - echo json_encode($_SESSION['return']); + case "self": + // No items, logged-in user, users and domain admins + if ($_SESSION['mailcow_cc_role'] == "domainadmin") { + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (domain_admin('edit', $attr) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Edit failed' + )); + } + exit(); } else { - echo json_encode(array( - 'type' => 'error', - 'msg' => 'Edit failed' - )); + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } } - exit(); } else { - if (isset($_SESSION['return'])) { - echo json_encode($_SESSION['return']); - } - else { - echo json_encode(array( - 'type' => 'success', - 'msg' => 'Task completed' - )); - } + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); } } - else { - echo json_encode(array( - 'type' => 'error', - 'msg' => 'Incomplete post data' - )); + elseif ($_SESSION['mailcow_cc_role'] == "user") { + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (edit_user_account($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' + )); + } + } + elseif ($_SESSION['mailcow_cc_role'] == "admin") { + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (edit_admin_account($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; } diff --git a/data/web/user.php b/data/web/user.php index 77ccc62d..b283894c 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -357,7 +357,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "use