Merge branch 'dev' into dns2

This commit is contained in:
Michael Kuron 2017-07-23 10:10:36 +02:00 committed by GitHub
commit fcbbfe71bb
41 changed files with 2194 additions and 812 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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);
};

View File

@ -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

View File

@ -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 <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
cat <<EOF > /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 <<EOF > /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 <<EOF > /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

View File

@ -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

View File

@ -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);
};

View File

@ -1,5 +1,5 @@
[zeyple]
log_file = /var/log/zeyple.log
log_file = /dev/null
[gpg]
home = /var/lib/zeyple/keys

View File

@ -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"]

View File

@ -0,0 +1,717 @@
--[[
Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
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

View File

@ -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

View File

@ -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);
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -78,4 +78,5 @@
//MySQL4DebugEnabled = YES;
//SOGoUIxDebugEnabled = YES;
//WODontZipResponse = YES;
WOLogFile = "/dev/sogo_log";
}

View File

@ -56,7 +56,7 @@ $tfa_data = get_tfa();
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button class="btn btn-default" id="edit_selected" data-id="admin" data-item="null" data-api-url='edit/admin' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
<button class="btn btn-default" id="edit_selected" data-id="admin" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
</div>
</div>
</form>
@ -121,6 +121,18 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-config">
<div class="row">
<div class="col-sm-2 hidden-xs">
<div id="scrollbox" class="list-group">
<a href="#dkim" class="list-group-item"><?=$lang['admin']['dkim_keys'];?></a>
<a href="#fwdhosts" class="list-group-item"><?=$lang['admin']['forwarding_hosts'];?></a>
<a href="#f2bparams" class="list-group-item"><?=$lang['admin']['f2b_parameters'];?></a>
<a href="#relayhosts" class="list-group-item">Relayhosts</a>
<a href="#top" class="list-group-item" style="border-top:1px dashed #dadada"> <?=$lang['admin']['to_top'];?></a>
</div>
</div>
<div class="col-sm-10">
<span class="anchor" id="dkim"></span>
<div class="panel panel-default">
<div class="panel-heading"><?=$lang['admin']['dkim_keys'];?></div>
<div class="panel-body">
@ -254,6 +266,7 @@ XYZ
</div>
</div>
<span class="anchor" id="fwdhosts"></span>
<div class="panel panel-default">
<div class="panel-heading"><?=$lang['admin']['forwarding_hosts'];?></div>
<div class="panel-body">
@ -291,6 +304,7 @@ XYZ
</div>
</div>
<span class="anchor" id="f2bparams"></span>
<div class="panel panel-default">
<div class="panel-heading"><?=$lang['admin']['f2b_parameters'];?></div>
<div class="panel-body">
@ -318,6 +332,48 @@ XYZ
</form>
</div>
</div>
<span class="anchor" id="relayhosts"></span>
<div class="panel panel-default">
<div class="panel-heading">Relayhosts</div>
<div class="panel-body">
<p style="margin-bottom:40px"><?=$lang['admin']['relayhosts_hint'];?></p>
<div class="table-responsive">
<table class="table table-striped table-condensed" id="relayhoststable"></table>
</div>
<div class="mass-actions-admin">
<div class="btn-group btn-group-sm">
<button type="button" id="toggle_multi_select_all" data-id="rlyhosts" class="btn btn-default"><?=$lang['mailbox']['toggle_all'];?></button>
<a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a id="edit_selected" data-id="rlyhosts" data-api-url='edit/relayhost' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
<li><a id="edit_selected" data-id="rlyhosts" data-api-url='edit/relayhost' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
<li role="separator" class="divider"></li>
<li><a id="delete_selected" data-id="rlyhosts" data-api-url='delete/relayhost' href="#"><?=$lang['admin']['remove'];?></a></li>
</ul>
</div>
</div>
<legend><?=$lang['admin']['add_relayhost'];?></legend>
<p class="help-block"><?=$lang['admin']['add_relayhost_add_hint'];?></p>
<form class="form-inline" data-id="rlyhost" role="form" method="post">
<div class="form-group">
<label for="hostname"><?=$lang['admin']['host'];?></label>
<input class="form-control" id="hostname" name="hostname" required>
</div>
<div class="form-group">
<label for="hostname"><?=$lang['admin']['username'];?></label>
<input class="form-control" id="username" name="username">
</div>
<div class="form-group">
<label for="hostname"><?=$lang['admin']['password'];?></label>
<input class="form-control" id="password" name="password">
</div>
<button class="btn btn-default" id="add_item" data-id="rlyhost" data-api-url='add/relayhost' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
</form>
</div>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tab-postfix-logs">

View File

@ -42,3 +42,14 @@ body.modal-open {
-webkit-transform:rotateX(180deg);
transform:rotateX(180deg);
}
.anchor {
display: block;
height: 65px;
margin-top: -65px;
visibility: hidden;
}
.scrollboxFixed {
position: fixed;
top: 65px;
z-index: 1;
}

View File

@ -27,3 +27,6 @@ table.footable>tbody>tr.footable-empty>td {
user-select: none;
padding:10px 0 10px 0;
}
.inputMissingAttr {
border-color: #FF4136;
}

View File

@ -25,9 +25,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
?>
<h4><?=$lang['edit']['alias'];?></h4>
<br />
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form class="form-horizontal" data-id="editalias" role="form" method="post">
<input type="hidden" value="0" name="active">
<input type="hidden" name="address" value="<?=htmlspecialchars($alias);?>">
<div class="form-group">
<label class="control-label col-sm-2" for="goto"><?=$lang['edit']['target_address'];?></label>
<div class="col-sm-10">
@ -43,7 +42,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="mailbox_edit_alias" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editalias" data-item="<?=$alias;?>" data-api-url='edit/alias' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>
@ -61,14 +60,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
$_GET["domainadmin"] != 'admin' &&
$_SESSION['mailcow_cc_role'] == "admin") {
$domain_admin = $_GET["domainadmin"];
$result = get_domain_admin_details($domain_admin);
$result = domain_admin('details', $domain_admin);
if (!empty($result)) {
?>
<h4><?=$lang['edit']['domain_admin'];?></h4>
<br />
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form class="form-horizontal" data-id="editdomainadmin" role="form" method="post">
<input type="hidden" value="0" name="active">
<input type="hidden" name="username" value="<?=htmlspecialchars($domain_admin);?>">
<div class="form-group">
<label class="control-label col-sm-2" for="username_new"><?=$lang['edit']['username'];?></label>
<div class="col-sm-10">
@ -122,7 +120,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="edit_domain_admin" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editdomainadmin" data-item="<?=$domain_admin;?>" data-api-url='edit/domain-admin' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>
@ -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)) {
?>
<h4><?=$lang['edit']['domain'];?></h4>
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form data-id="editdomain" class="form-horizontal" role="form" method="post">
<input type="hidden" value="0" name="active">
<input type="hidden" value="0" name="backupmx">
<input type="hidden" value="0" name="relay_all_recipients">
<input type="hidden" name="domain" value="<?=htmlspecialchars($domain);?>">
<div class="form-group">
<label class="control-label col-sm-2" for="description"><?=$lang['edit']['description'];?></label>
<div class="col-sm-10">
@ -178,6 +177,21 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
<label class="control-label col-sm-2" for="quota"><?=$lang['edit']['domain_quota'];?></label>
<div class="col-sm-10">
<input type="number" class="form-control" name="quota" id="quota" value="<?=intval($result['max_quota_for_domain'] / 1048576);?>">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="quota">Relayhost</label>
<div class="col-sm-10">
<select name="relayhost" id="relayhost" class="form-control">
<?php
foreach ($rlyhosts as $rlyhost) {
?>
<option value="<?=$rlyhost['id'];?>" <?=($result['relayhost'] == $rlyhost['id']) ? 'selected' : null;?>>ID <?=$rlyhost['id'];?>: <?=$rlyhost['hostname'];?> (<?=$rlyhost['username'];?>)</option>
<?php
}
?>
<option value="" <?=($result['relayhost'] == "0") ? 'selected' : null;?>>None</option>
</select>
</div>
</div>
<div class="form-group">
@ -203,7 +217,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="mailbox_edit_domain" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editdomain" data-item="<?=$domain;?>" data-api-url='edit/domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
</div>
</div>
</form>
@ -223,6 +237,23 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
}
?>
<hr>
<form data-id="domratelimit" class="form-inline well" method="post">
<div class="form-group">
<label class="control-label">Ratelimit</label>
<input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
</div>
<div class="form-group">
<select name="rl_frame" id="rl_frame" class="form-control">
<option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
<option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
<option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
</select>
</div>
<div class="form-group">
<button class="btn btn-default" id="edit_selected" data-id="domratelimit" data-item="<?=$domain;?>" data-api-url='edit/domain-ratelimit' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
</div>
</form>
<hr>
<div class="row">
<div class="col-sm-6">
<h4><?=$lang['user']['spamfilter_wl'];?></h4>
@ -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)) {
?>
<h4><?=$lang['edit']['edit_alias_domain'];?></h4>
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form class="form-horizontal" data-id="editaliasdomain" role="form" method="post">
<input type="hidden" value="0" name="active">
<input type="hidden" value="<?=$result['alias_domain'];?>" name="alias_domain">
<div class="form-group">
<label class="control-label col-sm-2" for="target_domain"><?=$lang['edit']['target_domain'];?></label>
<div class="col-sm-10">
@ -303,10 +334,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="mailbox_edit_alias_domain" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editaliasdomain" data-item="<?=$alias_domain;?>" data-api-url='edit/alias-domain' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>
<hr>
<form data-id="domratelimit" class="form-inline well" method="post">
<div class="form-group">
<label class="control-label">Ratelimit</label>
<input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
</div>
<div class="form-group">
<select name="rl_frame" id="rl_frame" class="form-control">
<option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
<option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
<option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
</select>
</div>
<div class="form-group">
<button class="btn btn-default" id="edit_selected" data-id="domratelimit" data-item="<?=$alias_domain;?>" data-api-url='edit/domain-ratelimit' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
</div>
</form>
<?php
if (!empty($dkim = dkim('details', $alias_domain))) {
?>
@ -334,10 +382,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
if (!empty($result)) {
?>
<h4><?=$lang['edit']['mailbox'];?></h4>
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form class="form-horizontal" data-id="editmailbox" role="form" method="post">
<input type="hidden" value="0" name="sender_acl">
<input type="hidden" value="0" name="active">
<input type="hidden" name="username" value="<?=htmlspecialchars($result['username']);?>">
<div class="form-group">
<label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?>:</label>
<div class="col-sm-10">
@ -355,7 +402,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
<div class="form-group">
<label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?>:</label>
<div class="col-sm-10">
<select data-width="100%" style="width:100%" id="sender_acl" name="sender_acl[]" size="10" multiple>
<select data-width="100%" style="width:100%" id="sender_acl" name="sender_acl" size="10" multiple>
<?php
$sender_acl_handles = mailbox('get', 'sender_acl_handles', $mailbox);
@ -426,7 +473,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="mailbox_edit_mailbox" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editmailbox" data-item="<?=htmlspecialchars($result['username']);?>" data-api-url='edit/mailbox' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>
@ -439,10 +486,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
if (!empty($result)) {
?>
<h4><?=$lang['edit']['resource'];?></h4>
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form class="form-horizontal" role="form" method="post" data-id="editresource">
<input type="hidden" value="0" name="active">
<input type="hidden" value="0" name="multiple_bookings">
<input type="hidden" name="name" value="<?=htmlspecialchars($result['name']);?>">
<div class="form-group">
<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
<div class="col-sm-10">
@ -475,7 +521,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="mailbox_edit_resource" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editresource" data-item="<?=htmlspecialchars($result['name']);?>" data-api-url='edit/resource' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>
@ -501,11 +547,10 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
if (!empty($result)) {
?>
<h4><?=$lang['edit']['syncjob'];?></h4>
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
<form class="form-horizontal" data-id="editsyncjob" role="form" method="post">
<input type="hidden" value="0" name="delete2duplicates">
<input type="hidden" value="0" name="delete1">
<input type="hidden" value="0" name="active">
<input type="hidden" name="id" value="<?=htmlspecialchars($result['id']);?>">
<div class="form-group">
<label class="control-label col-sm-2" for="host1"><?=$lang['edit']['hostname'];?></label>
<div class="col-sm-10">
@ -587,7 +632,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" name="edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
<button class="btn btn-success" id="edit_selected" data-id="editsyncjob" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/syncjob' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>

View File

@ -0,0 +1,507 @@
<?php
function domain_admin($_action, $_data = null) {
global $pdo;
global $lang;
switch ($_action) {
case 'add':
$username = strtolower(trim($_data['username']));
$password = $_data['password'];
$password2 = $_data['password2'];
$domains = (array)$_data['domains'];
$active = intval($_data['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) {
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;
}
}

View File

@ -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'])) {
$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 = $postarray['username'];
}
}
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;

View File

@ -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 {
@ -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'];

View File

@ -0,0 +1,179 @@
<?php
function relayhost($_action, $_data = null) {
global $pdo;
global $lang;
switch ($_action) {
case 'add':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => '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;
}
}

View File

@ -29,7 +29,7 @@
<link rel="shortcut icon" href="/favicon.png" type="image/png">
<link rel="icon" href="/favicon.png" type="image/png">
</head>
<body style="padding-top: 70px;">
<body style="padding-top: 70px;" id="top">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">

View File

@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
$db_version = "18052017_1017";
$db_version = "20072107_1029";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@ -60,6 +60,24 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"relayhosts" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
"hostname" => "VARCHAR(255) NOT NULL",
"username" => "VARCHAR(255) NOT NULL",
"password" => "VARCHAR(255) NOT NULL",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
"primary" => array(
"" => array("id")
),
"key" => array(
"hostname" => array("hostname")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"alias" => array(
"cols" => array(
"address" => "VARCHAR(255) NOT NULL",
@ -95,7 +113,7 @@ function init_db_schema() {
"mailboxes" => "INT(10) NOT NULL DEFAULT '0'",
"maxquota" => "BIGINT(20) NOT NULL DEFAULT '0'",
"quota" => "BIGINT(20) NOT NULL DEFAULT '102400'",
"transport" => "VARCHAR(255) NOT NULL",
"relayhost" => "VARCHAR(255) NOT NULL DEFAULT '0'",
"backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'",
"relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",

View File

@ -61,9 +61,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php';
include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';

View File

@ -1,6 +1,8 @@
<?php
// Start session
ini_set("session.cookie_httponly", 1);
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") {
ini_set("session.cookie_secure", 1);

View File

@ -54,53 +54,12 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
}
}
}
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") {
if (isset($_POST["edit_user_account"])) {
edit_user_account($_POST);
}
if (isset($_POST["edit_syncjob"])) {
mailbox('edit', 'syncjob', $_POST);
}
}
if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
if (isset($_POST["edit_domain_admin"])) {
edit_domain_admin($_POST);
}
if (isset($_POST["set_tfa"])) {
set_tfa($_POST);
}
if (isset($_POST["unset_tfa_key"])) {
unset_tfa_key($_POST);
}
if (isset($_POST["mailbox_edit_alias"])) {
mailbox('edit', 'alias', $_POST);
}
if (isset($_POST["mailbox_edit_domain"])) {
mailbox('edit', 'domain', $_POST);
}
if (isset($_POST["mailbox_edit_mailbox"])) {
mailbox('edit', 'mailbox', $_POST);
}
if (isset($_POST["mailbox_edit_alias_domain"])) {
mailbox('edit', 'alias_domain', $_POST);
}
if (isset($_POST["mailbox_edit_resource"])) {
mailbox('edit', 'resource', $_POST);
}
if (isset($_POST["mailbox_delete_domain"])) {
mailbox('delete', 'domain', $_POST);
}
if (isset($_POST["mailbox_delete_alias"])) {
mailbox('delete', 'delete_alias', $_POST);
}
if (isset($_POST["mailbox_delete_alias_domain"])) {
mailbox('delete', 'alias_domain', $_POST);
}
if (isset($_POST["mailbox_delete_mailbox"])) {
mailbox('delete', 'mailbox', $_POST);
}
if (isset($_POST["mailbox_delete_resource"])) {
mailbox('delete', 'resource', $_POST);
}
}
?>

View File

@ -328,6 +328,44 @@ jQuery(function($){
}
});
}
function draw_relayhosts() {
ft_forwardinghoststable = FooTable.init('#relayhoststable', {
"columns": [
{"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
{"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
{"name":"hostname","type":"text","title":lang.host,"style":{"width":"250px"}},
{"name":"username","title":lang.username,"breakpoints":"xs sm"},
{"name":"used_by_domains","title":lang.in_use_by, "type": "text","breakpoints":"xs sm"},
{"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
{"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
],
"rows": $.ajax({
dataType: 'json',
url: '/api/v1/get/relayhost/all',
jsonp: false,
error: function () {
console.log('Cannot draw forwarding hosts table');
},
success: function (data) {
$.each(data, function (i, item) {
item.action = '<div class="btn-group">' +
'<a href="#" id="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
'</div>';
item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
});
}
}),
"empty": lang.empty,
"paging": {
"enabled": true,
"limit": 5,
"size": log_pagination_size
},
"sorting": {
"enabled": true
}
});
}
function draw_rspamd_history() {
ft_postfix_logs = FooTable.init('#rspamd_history', {
"columns": [{
@ -504,5 +542,20 @@ jQuery(function($){
draw_fail2ban_logs();
draw_domain_admins();
draw_fwd_hosts();
draw_relayhosts();
draw_rspamd_history();
});
$(window).load(function(){
width = $("#scrollbox").width();
$(window).bind('scroll', function() {
if ($(window).scrollTop() > 70) {
$('#scrollbox').addClass('scrollboxFixed');
$("#scrollbox").css("width", width);
} else {
width = $("#scrollbox").width();
$('#scrollbox').removeClass('scrollboxFixed');
$("#scrollbox").removeAttr("style");
}
});
});

View File

@ -64,8 +64,23 @@ $(document).ready(function() {
// If clicked element #edit_selected is in a form with the same data-id as the button,
// we merge all input fields by {"name":"value"} into api-attr
if ($(this).closest("form").data('id') == id) {
var req_empty = false;
$(this).closest("form").find('select, textarea, input').each(function() {
if ($(this).prop('required')) {
if (!$(this).val()) {
req_empty = true;
$(this).addClass('inputMissingAttr');
} else {
$(this).removeClass('inputMissingAttr');
}
}
});
if (!req_empty) {
var attr_to_merge = $(this).closest("form").serializeObject();
var api_attr = $.extend(api_attr, attr_to_merge)
} else {
return false;
}
}
// If clicked element #edit_selected has data-item attribute, it is added to "items"
if (typeof $(this).data('item') !== 'undefined') {
@ -77,6 +92,7 @@ $(document).ready(function() {
}
if (typeof multi_data[id] == "undefined") return;
api_items = multi_data[id];
// alert(JSON.stringify(api_attr));
if (Object.keys(api_items).length !== 0) {
$.ajax({
type: "POST",

View File

@ -60,6 +60,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
case "relayhost":
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (relayhost('add', $attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Cannot add item'
));
}
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Cannot find attributes in post data'
));
}
break;
case "mailbox":
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
@ -426,7 +459,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
case "domain-admin":
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (add_domain_admin($attr) === false) {
if (domain_admin('add', $attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
@ -496,6 +529,42 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
break;
}
break;
case "relayhost":
switch ($object) {
case "all":
$relayhosts = relayhost('get');
if (!empty($relayhosts)) {
foreach ($relayhosts as $relayhost) {
if ($details = relayhost('details', $relayhost['id'])) {
$data[] = $details;
}
else {
continue;
}
}
if (!isset($data) || empty($data)) {
echo '{}';
}
else {
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
}
else {
echo '{}';
}
break;
default:
$data = relayhost('details', $object);
if (!isset($data) || empty($data)) {
echo '{}';
}
else {
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
break;
}
break;
case "logs":
switch ($object) {
case "dovecot":
@ -826,10 +895,10 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
case "domain-admin":
switch ($object) {
case "all":
$domain_admins = get_domain_admins();
$domain_admins = domain_admin('get');
if (!empty($domain_admins)) {
foreach ($domain_admins as $domain_admin) {
if ($details = get_domain_admin_details($domain_admin)) {
if ($details = domain_admin('details', $domain_admin)) {
$data[] = $details;
}
else {
@ -849,7 +918,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
break;
default:
$data = get_domain_admin_details($object);
$data = domain_admin('details', $object);
if (!isset($data) || empty($data)) {
echo '{}';
}
@ -930,6 +999,47 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
case "relayhost":
if (isset($_POST['items'])) {
$items = (array)json_decode($_POST['items'], true);
if (is_array($items)) {
if (relayhost('delete', array('id' => $items)) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Deletion of items/s failed'
));
}
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Cannot find id array in post data'
));
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Cannot find items in post data'
));
}
break;
case "syncjob":
if (isset($_POST['items'])) {
$items = (array)json_decode($_POST['items'], true);
@ -1385,7 +1495,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
if (isset($_POST['items'])) {
$items = (array)json_decode($_POST['items'], true);
if (is_array($items)) {
if (delete_domain_admin(array('username' => $items)) === false) {
if (domain_admin('delete', array('username' => $items)) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
@ -1470,6 +1580,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
case "relayhost":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
$attr = (array)json_decode($_POST['attr'], true);
$postarray = array_merge(array('id' => $items), $attr);
if (is_array($postarray['id'])) {
if (relayhost('edit', $postarray) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Edit failed'
));
}
exit();
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
break;
case "delimiter_action":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
@ -1603,6 +1757,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
}
break;
case "mailbox":
// sender_acl:0 removes all entries
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
$attr = (array)json_decode($_POST['attr'], true);
@ -1778,6 +1933,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
case "domain-ratelimit":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
$attr = (array)json_decode($_POST['attr'], true);
$postarray = array_merge(array('domain' => $items), $attr);
if (is_array($postarray['domain'])) {
if (mailbox('edit', 'domain_ratelimit', $postarray) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Edit failed'
));
}
exit();
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
break;
case "alias-domain":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
@ -1822,7 +2021,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
case "spam_score":
case "spam-score":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
$attr = (array)json_decode($_POST['attr'], true);
@ -1872,7 +2071,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
$attr = (array)json_decode($_POST['attr'], true);
$postarray = array_merge(array('username' => $items), $attr);
if (is_array($postarray['username'])) {
if (edit_domain_admin($postarray) === false) {
if (domain_admin('edit', $postarray) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
@ -1989,8 +2188,77 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
case "admin":
// No items as there is only one admin
case "self":
// No items, logged-in user, users and domain admins
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (domain_admin('edit', $attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Edit failed'
));
}
exit();
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
}
elseif ($_SESSION['mailcow_cc_role'] == "user") {
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (edit_user_account($attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Edit failed'
));
}
exit();
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
}
elseif ($_SESSION['mailcow_cc_role'] == "admin") {
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (edit_admin_account($attr) === false) {
@ -2023,6 +2291,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
'msg' => 'Incomplete post data'
));
}
}
break;
}
break;

View File

@ -486,16 +486,23 @@ $lang['admin']['time'] = 'Zeit';
$lang['admin']['priority'] = 'Gewichtung';
$lang['admin']['refresh'] = 'Neu laden';
$lang['admin']['logs'] = 'Logs';
$lang['admin']['to_top'] = 'Nach oben';
$lang['admin']['in_use_by'] = 'Verwendet von';
$lang['admin']['message'] = 'Nachricht';
$lang['admin']['forwarding_hosts'] = 'Weiterleitungs-Hosts';
$lang['admin']['forwarding_hosts_hint'] = 'Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt, optional kann er aber in den Spam-Ordner einsortiert werden. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem mailcow-Server eingerichtet wurde.';
$lang['admin']['forwarding_hosts_add_hint'] = 'Sie können entweder IPv4/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.';
$lang['admin']['relayhosts_hint'] = 'Erstellen Sie Relayhosts, um diese im Einstellungsdialog einer Domain auszuwählen.';
$lang['admin']['add_relayhost_add_hint'] = 'Bitte beachten Sie, dass Relayhost Anmeldedaten im Klartext gespeichert werden.';
$lang['admin']['host'] = 'Host';
$lang['admin']['source'] = 'Quelle';
$lang['admin']['add_forwarding_host'] = 'Weiterleitungs-Host hinzufügen';
$lang['admin']['add_relayhost'] = 'Relayhost hinzufügen';
$lang['delete']['remove_forwardinghost_warning'] = '<b>Warnung:</b> Sie entfernen den Weiterleitungs-Host <b>%s</b>!';
$lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt";
$lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt";
$lang['success']['relayhost_removed'] = "Relayhost %s wurde entfernt";
$lang['success']['relayhost_added'] = "Relayhost %s wurde hinzugefügt";
$lang['diagnostics']['dns_records'] = 'DNS-Einträge';
$lang['diagnostics']['dns_records_24hours'] = 'Bitte beachten Sie, dass es bis zu 24 Stunden dauern kann, bis Änderungen an Ihren DNS-Einträgen als aktueller Status auf dieser Seite dargestellt werden. Diese Seite ist nur als Hilfsmittel gedacht, um die korrekten Werte für DNS-Einträge zu anzuzeigen und zu überprüfen, ob die Daten im DNS hinterlegt sind.';
$lang['diagnostics']['dns_records_name'] = 'Name';

View File

@ -499,16 +499,23 @@ $lang['admin']['time'] = 'Time';
$lang['admin']['priority'] = 'Priority';
$lang['admin']['message'] = 'Message';
$lang['admin']['refresh'] = 'Refresh';
$lang['admin']['to_top'] = 'Back to top';
$lang['admin']['in_use_by'] = 'In use by';
$lang['admin']['logs'] = 'Logs';
$lang['admin']['forwarding_hosts'] = 'Forwarding Hosts';
$lang['admin']['forwarding_hosts_hint'] = 'Incoming messages are unconditionally accepted from any hosts listed here. These hosts are then not checked against DNSBLs or subjected to greylisting. Spam received from them is never rejected, but optionally it can be filed into the Junk folder. The most common use for this is to specify mail servers on which you have set up a rule that forwards incoming emails to your mailcow server.';
$lang['admin']['forwarding_hosts_add_hint'] = 'You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).';
$lang['admin']['relayhosts_hint'] = 'Define relayhosts here to be able to select them in a domains configuration dialog.';
$lang['admin']['add_relayhost_add_hint'] = 'Please be aware that relayhost authentication data will be stored as plain text.';
$lang['admin']['host'] = 'Host';
$lang['admin']['source'] = 'Source';
$lang['admin']['add_forwarding_host'] = 'Add Forwarding Host';
$lang['admin']['add_relayhost'] = 'Add Relayhost';
$lang['delete']['remove_forwardinghost_warning'] = '<b>Warning:</b> You are about to remove the forwarding host <b>%s</b>!';
$lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed";
$lang['success']['forwarding_host_added'] = "Forwarding host %s has been added";
$lang['success']['relayhost_removed'] = "Relayhost %s has been removed";
$lang['success']['relayhost_added'] = "Relayhost %s has been added";
$lang['diagnostics']['dns_records'] = 'DNS Records';
$lang['diagnostics']['dns_records_24hours'] = 'Please note that changes made to DNS may take up to 24 hours to correctly have their current state reflected on this page. It is intended as a way for you to easily see how to configure your DNS records and to check whether all your records are correctly stored in DNS.';
$lang['diagnostics']['dns_records_name'] = 'Name';

View File

@ -357,7 +357,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "use
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<form class="form-horizontal" role="form" method="post" autocomplete="off">
<form class="form-horizontal" data-id="pwchange" role="form" method="post" autocomplete="off">
<div class="form-group">
<label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
<div class="col-sm-5">
@ -380,7 +380,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "use
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" name="edit_<?=($_SESSION['mailcow_cc_role'] == "domainadmin") ? "domain_admin" : "user_account";?>" class="btn btn-sm btn-success"><?=$lang['user']['change_password'];?></button>
<button class="btn btn-default" id="edit_selected" data-id="pwchange" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#"><?=$lang['user']['change_password'];?></button>
</div>
</div>
</form>

View File

@ -80,7 +80,7 @@ services:
- clamd
rspamd-mailcow:
image: mailcow/rspamd:1.3
image: mailcow/rspamd:1.4
build: ./data/Dockerfiles/rspamd
command: >
/bin/bash -c "
@ -96,9 +96,6 @@ services:
- dkim-vol-1:/data/dkim
- rspamd-vol-1:/var/lib/rspamd
restart: always
logging:
options:
max-size: "5m"
dns:
- 172.22.1.254
dns_search: mailcow-network
@ -133,9 +130,6 @@ services:
- SMTPS_PORT=${SMTPS_PORT:-465}
- SMTP_PORT=${SMTP_PORT:-25}
restart: always
logging:
options:
max-size: "5m"
dns:
- 172.22.1.254
dns_search: mailcow-network
@ -145,7 +139,7 @@ services:
- phpfpm
sogo-mailcow:
image: mailcow/sogo:1.2
image: mailcow/sogo:1.3
build: ./data/Dockerfiles/sogo
depends_on:
unbound-mailcow:
@ -159,9 +153,6 @@ services:
volumes:
- ./data/conf/sogo/:/etc/sogo/
restart: always
logging:
options:
max-size: "5m"
dns:
- 172.22.1.254
dns_search: mailcow-network
@ -172,7 +163,7 @@ services:
- sogo
dovecot-mailcow:
image: mailcow/dovecot:1.3
image: mailcow/dovecot:1.4
build: ./data/Dockerfiles/dovecot
depends_on:
unbound-mailcow:
@ -187,16 +178,19 @@ services:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
logging:
options:
max-size: "5m"
ports:
- "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
- "${IMAP_PORT:-143}:143"
- "${IMAPS_PORT:-993}:993"
- "${POP_PORT:-110}:110"
- "${POPS_PORT:-995}:995"
- "${SIEVE_PORT:-4190}:4190"
restart: always
ulimits:
nproc: 65535
nofile:
soft: 20000
hard: 40000
dns:
- 172.22.1.254
dns_search: mailcow-network
@ -207,7 +201,7 @@ services:
- dovecot
postfix-mailcow:
image: mailcow/postfix:1.1
image: mailcow/postfix:1.3
build: ./data/Dockerfiles/postfix
depends_on:
unbound-mailcow:
@ -226,9 +220,6 @@ services:
- "${SMTPS_PORT:-465}:465"
- "${SUBMISSION_PORT:-587}:587"
restart: always
logging:
options:
max-size: "5m"
dns:
- 172.22.1.254
dns_search: mailcow-network
@ -275,9 +266,6 @@ services:
- ./data/conf/rspamd/dynmaps:/dynmaps:ro
- ./data/assets/ssl/:/etc/ssl/mail/:ro
- ./data/conf/nginx/:/etc/nginx/conf.d/:rw
logging:
options:
max-size: "5m"
ports:
- "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
- "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
@ -294,7 +282,7 @@ services:
acme-mailcow:
depends_on:
- nginx-mailcow
image: mailcow/acme:1.12
image: mailcow/acme:1.13
build: ./data/Dockerfiles/acme
dns:
- 172.22.1.254

View File

@ -68,6 +68,7 @@ IMAPS_PORT=993
POP_PORT=110
POPS_PORT=995
SIEVE_PORT=4190
DOVEADM_PORT=127.0.0.1:19991
# Your timezone
TZ=${TZ}

View File

@ -7,7 +7,7 @@ if [[ -z $(which git) ]]; then echo "Cannot find git, exiting."; exit 1; fi
if [[ -z $(which awk) ]]; then echo "Cannot find awk, exiting."; exit 1; fi
if [[ -z $(which sha1sum) ]]; then echo "Cannot find sha1sum, exiting."; exit 1; fi
CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN")
CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN" "DOVEADM_PORT")
echo >> mailcow.conf
for option in ${CONFIG_ARRAY[@]}; do
if [[ ${option} == "ADDITIONAL_SAN" ]]; then
@ -18,7 +18,12 @@ for option in ${CONFIG_ARRAY[@]}; do
elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "${COMPOSE_PROJECT_NAME}=mailcow-dockerized" >> mailcow.conf
echo "COMPOSE_PROJECT_NAME=mailcow-dockerized" >> mailcow.conf
fi
elif [[ ${option} == "DOVEADM_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
fi
elif ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"