diff --git a/.gitignore b/.gitignore index 9ea8b05b..b8558fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ data/conf/rspamd/override.d/* !data/conf/nginx/dynmaps.conf !data/conf/nginx/site.conf data/conf/nginx/*.conf +data/conf/dovecot/extra.conf diff --git a/README.md b/README.md index 6befab5f..42b2a3f1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -[![Donate (Bitcoin)](https://img.shields.io/badge/Donate-Bitcoin-blue.svg)](bitcoin:1E5rgzgA1sS3QH7r1ToWxRC3GEavfsGMrx) +**mailcow Bitcoin donations:** 1E5rgzgA1sS3QH7r1ToWxRC3GEavfsGMrx Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index b8098a14..616a6b81 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,20 +141,20 @@ 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 fi - ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${MAILCOW_HOSTNAME} | tr ' ' '\n' | sort | uniq -u )) + ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${ALL_VALIDATED[*]} | tr ' ' '\n' | sort | uniq -u )) if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then DATE=$(date +%Y-%m-%d_%H_%M_%S) echo "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..." mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/ [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/ - mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ - mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/cert.pem ]] && mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/private/${DATE}.bak/ # Keep key for TLSA 3 1 1 records fi diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index e2e02250..33d488b8 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -3,19 +3,16 @@ nodaemon=true [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps -redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autostart=true -stdout_syslog=true [program:dovecot] command=/usr/local/sbin/dovecot -F autorestart=true -[program:logfiles] -command=/usr/bin/tail -f /var/log/combined.log -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 - [program:cron] command=/usr/sbin/cron -f autorestart=true diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf index b8cc44fd..0257d6a1 100644 --- a/data/Dockerfiles/dovecot/syslog-ng.conf +++ b/data/Dockerfiles/dovecot/syslog-ng.conf @@ -13,9 +13,8 @@ source s_src { unix-stream("/dev/log"); internal(); }; - -destination d_combined { file("/var/log/combined.log"); }; -destination d_redis_persistent_log { +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { redis( host("redis-mailcow") persist-name("redis1") @@ -34,8 +33,8 @@ destination d_redis_f2b_channel { filter f_mail { facility(mail); }; log { source(s_src); - destination(d_combined); + destination(d_stdout); filter(f_mail); - destination(d_redis_persistent_log); + destination(d_redis_ui_log); destination(d_redis_f2b_channel); }; diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 283adbe6..2e0e73df 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -25,14 +25,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ syslog-ng \ syslog-ng-core \ syslog-ng-mod-redis \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && touch /etc/default/locale RUN addgroup --system --gid 600 zeyple RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple RUN touch /var/log/zeyple.log && chown zeyple: /var/log/zeyple.log -RUN touch /etc/default/locale - COPY zeyple.py /usr/local/bin/zeyple.py COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 640538b0..9837eeef 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -20,12 +20,26 @@ dbname = ${DBNAME} query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF -cat < /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf +cat < /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out'; +query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps + FROM ( + SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address = '%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain = '%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'smtp:') AS 'transport' + UNION ALL + SELECT hostname AS transport FROM relayhosts LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id WHERE relayhosts.active = '1' AND domain = '%d' OR domain IN (SELECT target_domain FROM alias_domain WHERE alias_domain = '%d') + ) + AS transport_view; +EOF + +cat < /opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts WHERE id IN (SELECT relayhost FROM domain WHERE CONCAT('@', domain) = '%s'); EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -110,6 +124,5 @@ if [[ $? != 0 ]]; then exit 1 else postfix -c /opt/postfix/conf start - supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 0968bb0a..55e76a95 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -3,19 +3,16 @@ nodaemon=true [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps -redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autostart=true -stdout_syslog=true [program:postfix] command=/opt/postfix.sh autorestart=true -[program:postfix-maillog] -command=/bin/tail -f /var/log/zeyple.log /var/log/combined.log -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 - [unix_http_server] file=/var/tmp/supervisord.sock chmod=0770 diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index c4bb63ba..808b06b7 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -13,9 +13,8 @@ source s_src { unix-stream("/dev/log"); internal(); }; - -destination d_combined { file("/var/log/combined.log"); }; -destination d_redis_persistent_log { +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { redis( host("redis-mailcow") persist-name("redis1") @@ -34,8 +33,8 @@ destination d_redis_f2b_channel { filter f_mail { facility(mail); }; log { source(s_src); - destination(d_combined); + destination(d_stdout); filter(f_mail); - destination(d_redis_persistent_log); + destination(d_redis_ui_log); destination(d_redis_f2b_channel); }; diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf index 7f039582..cc176a0e 100644 --- a/data/Dockerfiles/postfix/zeyple.conf +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -1,5 +1,5 @@ [zeyple] -log_file = /var/log/zeyple.log +log_file = /dev/null [gpg] home = /var/lib/zeyple/keys 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 + + diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index b76173fe..30392e3b 100644 --- a/data/Dockerfiles/sogo/supervisord.conf +++ b/data/Dockerfiles/sogo/supervisord.conf @@ -3,9 +3,11 @@ nodaemon=true [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps -redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autostart=true -stdout_syslog=true priority=1 [program:cron] @@ -22,22 +24,24 @@ priority=4 [program:reconf-domains] command=/reconf-domains.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 priority=3 autorestart=true [program:sogo] command="/usr/sbin/sogod" user=sogo +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autorestart = unexpected autostart = false priority=5 -[program:sogo-syslog] -command=/usr/bin/tail -f /var/log/combined.log -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 -priority=6 - [inet_http_server] port=9191 diff --git a/data/Dockerfiles/sogo/syslog-ng.conf b/data/Dockerfiles/sogo/syslog-ng.conf index 4d17d9b1..264bca44 100644 --- a/data/Dockerfiles/sogo/syslog-ng.conf +++ b/data/Dockerfiles/sogo/syslog-ng.conf @@ -1,4 +1,4 @@ -@version: 3.5 +@version: 3.8 @include "scl.conf" options { chain_hostnames(off); @@ -14,12 +14,10 @@ source s_src { internal(); }; source s_sogo { - file("/var/log/sogo/sogo.log"); + pipe("/dev/sogo_log" owner(sogo) group(sogo)); }; -destination d_combined { - file("/var/log/combined.log"); -}; -destination d_redis_persistent_log { +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { redis( host("redis-mailcow") persist-name("redis1") @@ -37,11 +35,11 @@ destination d_redis_f2b_channel { }; log { source(s_sogo); - source(s_src); - destination(d_combined); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); }; log { source(s_sogo); - destination(d_redis_persistent_log); - destination(d_redis_f2b_channel); + source(s_src); + destination(d_stdout); }; diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 2fc271e8..2d416d21 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -1,3 +1,6 @@ +# -------------------------------------------------------------------------- +# Please create a file "extra.conf" for persistent overrides to dovecot.conf +# -------------------------------------------------------------------------- auth_mechanisms = plain login #mail_debug = yes log_path = syslog @@ -31,6 +34,12 @@ passdb { args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } +# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing) +service doveadm { + inet_listener { + port = 12345 + } +} namespace inbox { inbox = yes location = @@ -256,3 +265,4 @@ service imap-postlogin { unix_listener imap-postlogin { } } +!include_try /usr/local/etc/dovecot/extra.conf diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 82bfae85..fe6fd3ab 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -39,11 +39,11 @@ postscreen_greet_ttl = 2d postscreen_greet_wait = 3s postscreen_non_smtp_command_enable = no postscreen_pipelining_enable = no -proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $smtpd_sender_login_maps +proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $smtpd_sender_login_maps queue_run_delay = 300s relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf -sender_dependent_default_transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf +sender_dependent_default_transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt smtp_tls_cert_file = /etc/ssl/mail/cert.pem smtp_tls_key_file = /etc/ssl/mail/key.pem @@ -94,3 +94,8 @@ mydestination = localhost.localdomain, localhost #content_filter=zeyple # Prefere IPv4, useful for v4-only envs smtp_address_preference = ipv4 +smtp_sender_dependent_authentication = yes +smtp_sasl_auth_enable = yes +smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf +smtp_sasl_security_options = +smtp_sasl_mechanism_filter = plain, login diff --git a/data/conf/rspamd/local.d/arc.conf b/data/conf/rspamd/local.d/arc.conf index 578056f9..e8d95871 100644 --- a/data/conf/rspamd/local.d/arc.conf +++ b/data/conf/rspamd/local.d/arc.conf @@ -5,7 +5,7 @@ allow_hdrfrom_mismatch = false; # If true, multiple from headers are allowed (but only first is used) allow_hdrfrom_multiple = true; # If true, username does not need to contain matching domain -allow_username_mismatch = true; +allow_username_mismatch = false; # If false, messages from authenticated users are not selected for signing auth_only = true; # Default path to key, can include '$domain' and '$selector' variables 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; diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf index 32dc1e5e..151f5aa3 100644 --- a/data/conf/sogo/sogo.conf +++ b/data/conf/sogo/sogo.conf @@ -78,4 +78,5 @@ //MySQL4DebugEnabled = YES; //SOGoUIxDebugEnabled = YES; //WODontZipResponse = YES; + WOLogFile = "/dev/sogo_log"; } diff --git a/data/web/admin.php b/data/web/admin.php index 0e0003a8..bb53e61e 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -56,7 +56,7 @@ $tfa_data = get_tfa();
- +
@@ -121,6 +121,18 @@ $tfa_data = get_tfa();
+
+ +
+
@@ -253,7 +265,8 @@ XYZ
- + +
@@ -291,6 +304,7 @@ XYZ
+
@@ -318,6 +332,48 @@ XYZ
+ + +
+
Relayhosts
+
+

+
+
+
+
+
+ + + +
+
+ +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
diff --git a/data/web/css/admin.css b/data/web/css/admin.css index 7f8897a0..aa07d7a3 100644 --- a/data/web/css/admin.css +++ b/data/web/css/admin.css @@ -41,4 +41,15 @@ body.modal-open { -moz-transform:rotateX(180deg); -webkit-transform:rotateX(180deg); transform:rotateX(180deg); -} \ No newline at end of file +} +.anchor { + display: block; + height: 65px; + margin-top: -65px; + visibility: hidden; +} +.scrollboxFixed { + position: fixed; + top: 65px; + z-index: 1; +} 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..eb0eb375 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,15 @@ 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); + $rlyhosts = relayhost('get'); if (!empty($result)) { ?>

-
"> + -
@@ -180,6 +179,21 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
+
+ +
+ +
+
@@ -203,7 +217,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -223,6 +237,23 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm } ?>
+
+
+ + +
+
+ +
+
+ +
+
+

@@ -282,12 +313,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 +334,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
+
+
+
+ + +
+
+ +
+
+ +
+
@@ -334,10 +382,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if (!empty($result)) { ?>

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

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

-
"> + -
@@ -587,7 +632,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..3f302783 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -135,9 +135,9 @@ function mailbox($_action, $_type, $_data = null) { return false; } try { - $stmt = $pdo->prepare("SELECT `user2`, `user1` FROM `imapsync` - WHERE `user2` = :user2 AND `user1` = :user1"); - $stmt->execute(array(':user1' => $user1, ':user2' => $username)); + $stmt = $pdo->prepare("SELECT '1' FROM `imapsync` + WHERE `user2` = :user2 AND `user1` = :user1 AND `host1` = :host1"); + $stmt->execute(array(':user1' => $user1, ':user2' => $username, ':host1' => $host1)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); } catch(PDOException $e) { @@ -260,8 +260,8 @@ function mailbox($_action, $_type, $_data = null) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `active`, `relay_all_recipients`) - VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :active, :relay_all_recipients)"); + $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `backupmx`, `active`, `relay_all_recipients`) + VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, :backupmx, :active, :relay_all_recipients)"); $stmt->execute(array( ':domain' => $domain, ':description' => $description, @@ -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,10 @@ 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']; + $relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost']; $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); @@ -1476,6 +1532,7 @@ function mailbox($_action, $_type, $_data = null) { `active` = :active, `quota` = :quota, `maxquota` = :maxquota, + `relayhost` = :relayhost, `mailboxes` = :mailboxes, `aliases` = :aliases, `description` = :description @@ -1486,6 +1543,7 @@ function mailbox($_action, $_type, $_data = null) { ':active' => $active, ':quota' => $quota, ':maxquota' => $maxquota, + ':relayhost' => $relayhost, ':mailboxes' => $mailboxes, ':aliases' => $aliases, ':description' => $description, @@ -1524,7 +1582,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 +1646,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 +1662,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 +1815,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 +2321,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 { @@ -2394,7 +2473,7 @@ function mailbox($_action, $_type, $_data = null) { ':domain' => $_data )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($row)) { + if (!empty($row)) { $_data = $row['target_domain']; } $stmt = $pdo->prepare("SELECT @@ -2404,6 +2483,7 @@ function mailbox($_action, $_type, $_data = null) { `mailboxes`, `maxquota`, `quota`, + `relayhost`, `relay_all_recipients` as `relay_all_recipients_int`, `backupmx` as `backupmx_int`, `active` as `active_int`, @@ -2438,6 +2518,7 @@ function mailbox($_action, $_type, $_data = null) { $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes']; $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576; $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576; + $domaindata['relayhost'] = $row['relayhost']; $domaindata['backupmx'] = $row['backupmx']; $domaindata['backupmx_int'] = $row['backupmx_int']; $domaindata['active'] = $row['active']; diff --git a/data/web/inc/functions.relayhost.inc.php b/data/web/inc/functions.relayhost.inc.php new file mode 100644 index 00000000..249dacc3 --- /dev/null +++ b/data/web/inc/functions.relayhost.inc.php @@ -0,0 +1,179 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $hostname = trim($_data['hostname']); + $username = str_replace(':', '\:', trim($_data['username'])); + $password = str_replace(':', '\:', trim($_data['password'])); + if (empty($hostname)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid host specified: '. htmlspecialchars($host) + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `relayhosts` (`hostname`, `username` ,`password`, `active`) + VALUES (:hostname, :username, :password, :active)"); + $stmt->execute(array( + ':hostname' => $hostname, + ':username' => $username, + ':password' => $password, + ':active' => '1' + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['relayhost_added'], htmlspecialchars(implode(', ', $hosts))) + ); + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $ids = (array)$_data['id']; + foreach ($ids as $id) { + $is_now = relayhost('details', $id); + if (!empty($is_now)) { + $hostname = (!empty($_data['hostname'])) ? trim($_data['hostname']) : $is_now['hostname']; + $username = (!empty($_data['username'])) ? trim($_data['username']) : $is_now['username']; + $password = (!empty($_data['password'])) ? trim($_data['password']) : $is_now['password']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Relayhost invalid' + ); + return false; + } + try { + $stmt = $pdo->prepare("UPDATE `relayhosts` SET + `hostname` = :hostname, + `username` = :username, + `password` = :password, + `active` = :active + WHERE `id` = :id"); + $stmt->execute(array( + ':id' => $id, + ':hostname' => $hostname, + ':username' => $username, + ':password' => $password, + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars(implode(', ', $hostnames))) + ); + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $ids = (array)$_data['id']; + foreach ($ids as $id) { + try { + $stmt = $pdo->prepare("DELETE FROM `relayhosts` WHERE `id`= :id"); + $stmt->execute(array(':id' => $id)); + $stmt = $pdo->prepare("UPDATE `domain` SET `relayhost` = '0' WHERE `relayhost`= :id"); + $stmt->execute(array(':id' => $id)); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['relayhost_removed'], htmlspecialchars(implode(', ', $hostnames))) + ); + break; + case 'get': + if ($_SESSION['mailcow_cc_role'] != "admin") { + return false; + } + $relayhosts = array(); + try { + $stmt = $pdo->query("SELECT `id`, `hostname`, `username` FROM `relayhosts`"); + $relayhosts = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $relayhosts; + break; + case 'details': + if ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + $relayhostdata = array(); + try { + $stmt = $pdo->prepare("SELECT `id`, + `hostname`, + `username`, + `password`, + `active` AS `active_int`, + CONCAT(LEFT(`password`, 3), '...') AS `password_short`, + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` + FROM `relayhosts` + WHERE `id` = :id"); + $stmt->execute(array(':id' => $_data)); + $relayhostdata = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!empty($relayhostdata)) { + $stmt = $pdo->prepare("SELECT GROUP_CONCAT(`domain` SEPARATOR ', ') AS `used_by_domains` FROM `domain` WHERE `relayhost` = :id"); + $stmt->execute(array(':id' => $_data)); + $used_by_domains = $stmt->fetch(PDO::FETCH_ASSOC)['used_by_domains']; + $used_by_domains = (empty($used_by_domains)) ? '' : $used_by_domains; + $relayhostdata['used_by_domains'] = $used_by_domains; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $relayhostdata; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index 8e5b6d07..97125d53 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -29,7 +29,7 @@ - +