diff --git a/.gitignore b/.gitignore index b8558fdb..9de43f40 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ data/conf/rspamd/override.d/* !data/conf/nginx/site.conf data/conf/nginx/*.conf data/conf/dovecot/extra.conf +data/conf/rspamd/custom/* diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index ff2429ae..a69a1ca4 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -77,9 +77,12 @@ while true; do # Container ids may have changed CONTAINERS_RESTART=($(curl --silent --unix-socket /var/run/docker.sock http/containers/json | jq -rc 'map(select(.Names[] | contains ("nginx-mailcow") or contains ("postfix-mailcow") or contains ("dovecot-mailcow"))) | .[] .Id' | tr "\n" " ")) - while read line; do - SQL_DOMAIN_ARR+=("${line}") + while read domain; do + SQL_DOMAIN_ARR+=("${domain}") done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -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) @@ -123,6 +126,9 @@ while true; do fi for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do + if [[ ${SAN} == ${MAILCOW_HOSTNAME} ]]; then + continue + fi A_SAN=$(dig A ${SAN} +short | tail -n 1) if [[ ! -z ${A_SAN} ]]; then echo "Found A record for ${SAN}: ${A_SAN}" @@ -138,20 +144,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=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) if [[ -z ${ALL_VALIDATED[*]} ]]; then echo "Cannot validate hostnames, skipping Let's Encrypt..." exit 0 fi - ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${MAILCOW_HOSTNAME} | tr ' ' '\n' | sort | uniq -u )) + ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${ALL_VALIDATED[*]} | tr ' ' '\n' | sort | uniq -u )) if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then DATE=$(date +%Y-%m-%d_%H_%M_%S) echo "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..." mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/ [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/ - mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ - mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/cert.pem ]] && mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/private/${DATE}.bak/ # Keep key for TLSA 3 1 1 records fi diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index 48edeae1..aa50b807 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -15,6 +15,8 @@ RUN apk add --update \ && chmod 750 /run/clamav \ && sed -i '/Foreground yes/s/^#//g' /etc/clamav/clamd.conf \ && sed -i '/TCPSocket 3310/s/^#//g' /etc/clamav/clamd.conf \ + && sed -i 's/#PhishingSignatures yes/PhishingSignatures no/g' /etc/clamav/clamd.conf \ + && sed -i 's/#PhishingScanURLs yes/PhishingScanURLs no/g' /etc/clamav/clamd.conf \ && sed -i '/Foreground yes/s/^#//g' /etc/clamav/freshclam.conf # Port provision diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 33f64382..d7f9cf38 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -3,8 +3,8 @@ LABEL maintainer "Andre Peters " ARG DEBIAN_FRONTEND=noninteractive ENV LC_ALL C -ENV DOVECOT_VERSION 2.2.31 -ENV PIGEONHOLE_VERSION 0.4.18 +ENV DOVECOT_VERSION 2.2.32 +ENV PIGEONHOLE_VERSION 0.4.20 RUN apt-get update && apt-get -y install \ automake \ @@ -40,6 +40,7 @@ RUN apt-get update && apt-get -y install \ libtest-pod-perl \ libtest-simple-perl \ libunicode-string-perl \ + libproc-processtable-perl \ liburi-perl \ lzma-dev \ make \ @@ -67,7 +68,7 @@ RUN curl https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz | RUN cpanm Data::Uniqid Mail::IMAPClient String::Util RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync -RUN echo '30 3 * * * vmail /usr/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync +RUN echo '30 3 * * * vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY imapsync /usr/local/bin/imapsync diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index a07a3896..4e9fe14b 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -74,4 +74,7 @@ chown -R vmail:vmail /var/vmail/sieve # Fix more than 1 hardlink issue touch /etc/crontab /etc/cron.*/* +# Clean old PID if any +[[ -f /usr/local/var/run/dovecot/master.pid ]] && rm /usr/local/var/run/dovecot/master.pid + exec "$@" diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl index 27419aac..132e536e 100755 --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -2,11 +2,20 @@ use DBI; use LockFile::Simple qw(lock trylock unlock); +use Proc::ProcessTable; use Data::Dumper qw(Dumper); use IPC::Run 'run'; use String::Util 'trim'; use File::Temp; +my $t = Proc::ProcessTable->new; +my $imapsync_running = grep { $_->{cmndline} =~ /^\/usr\/bin\/perl \/usr\/local\/bin\/imapsync\s/ } @{$t->table}; +if ($imapsync_running eq 1) +{ + print "imapsync is active, exiting..."; + exit; +} + $DBNAME = ''; $DBUSER = ''; $DBPASS = ''; @@ -16,12 +25,15 @@ $dsn = "DBI:mysql:database=" . $DBNAME . ";host=mysql"; $lock_file = $run_dir . "/imapsync_busy"; $lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1); $lockmgr->lock($lock_file) || die "can't lock ${lock_file}"; -$dbh = DBI->connect($dsn, $DBUSER, $DBPASS); +$dbh = DBI->connect($dsn, $DBUSER, $DBPASS, { + mysql_auto_reconnect => 1, + mysql_enable_utf8mb4 => 1 +}); open my $file, '<', "/etc/sogo/sieve.creds"; my $creds = <$file>; close $file; my ($master_user, $master_pass) = split /:/, $creds; -my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL) ORDER BY last_run"); +my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1, delete2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL) ORDER BY last_run"); $sth->execute(); my $row; @@ -40,6 +52,7 @@ while ($row = $sth->fetchrow_arrayref()) { $maxage = @$row[10]; $subfolder2 = @$row[11]; $delete1 = @$row[12]; + $delete2 = @$row[13]; if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } @@ -59,6 +72,7 @@ while ($row = $sth->fetchrow_arrayref()) { ($maxage eq "0" ? () : ('--maxage', $maxage)), ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), ($delete1 ne "1" ? () : ('--delete')), + ($delete2 ne "1" ? () : ('--delete2')), (!defined($enc1) ? () : ($enc1)), "--host1", $host1, "--user1", $user1, diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf index 0257d6a1..292efc7d 100644 --- a/data/Dockerfiles/dovecot/syslog-ng.conf +++ b/data/Dockerfiles/dovecot/syslog-ng.conf @@ -30,6 +30,14 @@ destination d_redis_f2b_channel { command("PUBLISH" "F2B_CHANNEL" "$MESSAGE") ); }; +destination d_redis_cleanup { + redis( + host("redis-mailcow") + persist-name("redis3") + port(6379) + command("LTRIM" "DOVECOT_MAILLOG" "0" "9999") + ); +}; filter f_mail { facility(mail); }; log { source(s_src); @@ -37,4 +45,6 @@ log { filter(f_mail); destination(d_redis_ui_log); destination(d_redis_f2b_channel); + destination(d_redis_cleanup); + }; diff --git a/data/Dockerfiles/fail2ban/logwatch.py b/data/Dockerfiles/fail2ban/logwatch.py index d431a072..9615d53a 100644 --- a/data/Dockerfiles/fail2ban/logwatch.py +++ b/data/Dockerfiles/fail2ban/logwatch.py @@ -23,11 +23,10 @@ pubsub = r.pubsub() RULES = {} RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' -RULES[3] = '-login: Disconnected \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' -RULES[4] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' -RULES[5] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' -RULES[6] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' -RULES[7] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' +RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' +RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' +RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' +RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' r.setnx("F2B_BAN_TIME", "1800") r.setnx("F2B_MAX_ATTEMPTS", "10") diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 640538b0..2456f3d9 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -3,6 +3,10 @@ trap "postfix stop" EXIT [[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/ +if [[ -z $(grep null /etc/aliases) ]]; then + echo null: /dev/null >> /etc/aliases; + newaliases; +fi cat < /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf user = ${DBUSER} @@ -20,12 +24,26 @@ dbname = ${DBNAME} query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF -cat < /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf +cat < /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out'; +query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps + FROM ( + SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address = '%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain = '%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'smtp:') AS 'transport' + UNION ALL + SELECT hostname AS transport FROM relayhosts LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id WHERE relayhosts.active = '1' AND domain = '%d' OR domain IN (SELECT target_domain FROM alias_domain WHERE alias_domain = '%d') + ) + AS transport_view; +EOF + +cat < /opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts WHERE id IN (SELECT relayhost FROM domain WHERE CONCAT('@', domain) = '%s'); EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -110,6 +128,5 @@ if [[ $? != 0 ]]; then exit 1 else postfix -c /opt/postfix/conf start - supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index 808b06b7..cfb76a16 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -30,6 +30,14 @@ destination d_redis_f2b_channel { command("PUBLISH" "F2B_CHANNEL" "$MESSAGE") ); }; +destination d_redis_cleanup { + redis( + host("redis-mailcow") + persist-name("redis3") + port(6379) + command("LTRIM" "POSTFIX_MAILLOG" "0" "9999") + ); +}; filter f_mail { facility(mail); }; log { source(s_src); @@ -37,4 +45,5 @@ log { filter(f_mail); destination(d_redis_ui_log); destination(d_redis_f2b_channel); + destination(d_redis_cleanup); }; diff --git a/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh b/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh index 4ad5ab32..ab066d89 100755 --- a/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh +++ b/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh @@ -6,7 +6,7 @@ while read QUERY; do echo "500 dunno" continue fi - result=$(curl -s http://nginx:8081/forwardinghosts.php?host=${QUERY[1]}) + result=$(curl -s http://172.22.1.251:8081/forwardinghosts.php?host=${QUERY[1]}) logger -t whitelist_forwardinghosts -p mail.info "Look up ${QUERY[1]} on whitelist, result $result" echo ${result} done diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 408a7a1d..32e0fe30 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -8,8 +8,8 @@ RUN apt-get update && apt-get install -y \ ca-certificates \ gnupg2 \ apt-transport-https \ - && apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \ - && echo "deb https://rspamd.com/apt-stable/ stretch main" > /etc/apt/sources.list.d/rspamd.list \ + && apt-key adv --fetch-keys https://rspamd.com/apt/gpg.key \ + && echo "deb https://rspamd.com/apt/ stretch main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update && apt-get install -y rspamd \ && rm -rf /var/lib/apt/lists/* \ && echo '.include $LOCAL_CONFDIR/local.d/rspamd.conf.local' > /etc/rspamd/rspamd.conf.local \ @@ -19,6 +19,8 @@ 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 lua_util.lua /usr/share/rspamd/lib/lua_util.lua COPY docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/rspamd/lua_util.lua b/data/Dockerfiles/rspamd/lua_util.lua new file mode 100644 index 00000000..a9abd901 --- /dev/null +++ b/data/Dockerfiles/rspamd/lua_util.lua @@ -0,0 +1,152 @@ +local exports = {} +local lpeg = require 'lpeg' + +local split_grammar = {} +local function rspamd_str_split(s, sep) + local gr = split_grammar[sep] + + if not gr then + local _sep = lpeg.P(sep) + local elem = lpeg.C((1 - _sep)^0) + local p = lpeg.Ct(elem * (_sep * elem)^0) + gr = p + split_grammar[sep] = gr + end + + return gr:match(s) +end + +exports.rspamd_str_split = rspamd_str_split + +local space = lpeg.S' \t\n\v\f\r' +local nospace = 1 - space +local ptrim = space^0 * lpeg.C((space^0 * nospace^1)^0) +local match = lpeg.match +exports.rspamd_str_trim = function(s) + return match(ptrim, s) +end + +-- Robert Jay Gould http://lua-users.org/wiki/SimpleRound +exports.round = function(num, numDecimalPlaces) + local mult = 10^(numDecimalPlaces or 0) + return math.floor(num * mult) / mult +end + +exports.template = function(tmpl, keys) + local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" } + local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) } + local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") } + + local template_grammar = lpeg.Cs((var + var_braced + 1)^0) + + return lpeg.match(template_grammar, tmpl) +end + +exports.remove_email_aliases = function(email_addr) + local function check_gmail_user(addr) + -- Remove all points + local no_dots_user = string.gsub(addr.user, '%.', '') + local cap, pluses = string.match(no_dots_user, '^([^%+][^%+]*)(%+.*)$') + if cap then + return cap, rspamd_str_split(pluses, '+'), nil + elseif no_dots_user ~= addr.user then + return no_dots_user,{},nil + end + + return nil + end + + local function check_address(addr) + if addr.user then + local cap, pluses = string.match(addr.user, '^([^%+][^%+]*)(%+.*)$') + if cap then + return cap, rspamd_str_split(pluses, '+'), nil + end + end + + return nil + end + + local function set_addr(addr, new_user, new_domain) + if new_user then + addr.user = new_user + end + if new_domain then + addr.domain = new_domain + end + + if addr.domain then + addr.addr = string.format('%s@%s', addr.user, addr.domain) + else + addr.addr = string.format('%s@', addr.user) + end + + if addr.name and #addr.name > 0 then + addr.raw = string.format('"%s" <%s>', addr.name, addr.addr) + else + addr.raw = string.format('<%s>', addr.addr) + end + end + + local function check_gmail(addr) + local nu, tags, nd = check_gmail_user(addr) + + if nu then + return nu, tags, nd + end + + return nil + end + + local function check_googlemail(addr) + local nd = 'gmail.com' + local nu, tags = check_gmail_user(addr) + + if nu then + return nu, tags, nd + end + + return nil, nil, nd + end + + local specific_domains = { + ['gmail.com'] = check_gmail, + ['googlemail.com'] = check_googlemail, + } + + if email_addr then + if email_addr.domain and specific_domains[email_addr.domain] then + local nu, tags, nd = specific_domains[email_addr.domain](email_addr) + if nu or nd then + set_addr(email_addr, nu, nd) + + return nu, tags + end + else + local nu, tags, nd = check_address(email_addr) + if nu or nd then + set_addr(email_addr, nu, nd) + + return nu, tags + end + end + + return nil + end +end + +exports.is_rspamc_or_controller = function(task) + local ua = task:get_request_header('User-Agent') or '' + local pwd = task:get_request_header('Password') + local is_rspamc = false + if tostring(ua) == 'rspamc' or pwd then is_rspamc = true end + + return is_rspamc +end + +local unpack_function = table.unpack or unpack +exports.unpack = function(t) + return unpack_function(t) +end + +return exports diff --git a/data/Dockerfiles/rspamd/milter_headers.lua b/data/Dockerfiles/rspamd/milter_headers.lua deleted file mode 100644 index 6ad7957a..00000000 --- a/data/Dockerfiles/rspamd/milter_headers.lua +++ /dev/null @@ -1,387 +0,0 @@ ---[[ -Copyright (c) 2016, Andrew Lewis -Copyright (c) 2016, Vsevolod Stakhov - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -]]-- - -if confighelp then - return -end - --- A plugin that provides common header manipulations - -local logger = require "rspamd_logger" -local util = require "rspamd_util" -local N = 'milter_headers' -local E = {} - -local HOSTNAME = util.get_hostname() - -local settings = { - skip_local = false, - skip_authenticated = false, - routines = { - ['x-spamd-result'] = { - header = 'X-Spamd-Result', - remove = 1, - }, - ['x-rspamd-server'] = { - header = 'X-Rspamd-Server', - remove = 1, - }, - ['x-rspamd-queue-id'] = { - header = 'X-Rspamd-Queue-Id', - remove = 1, - }, - ['spam-header'] = { - header = 'Deliver-To', - value = 'Junk', - remove = 1, - }, - ['x-virus'] = { - header = 'X-Virus', - remove = 1, - symbols = {}, -- needs config - }, - ['x-spamd-bar'] = { - header = 'X-Spamd-Bar', - positive = '+', - negative = '-', - neutral = '/', - remove = 1, - }, - ['x-spam-level'] = { - header = 'X-Spam-Level', - char = '*', - remove = 1, - }, - ['x-spam-status'] = { - header = 'X-Spam-Status', - remove = 1, - }, - ['authentication-results'] = { - header = 'Authentication-Results', - remove = 1, - spf_symbols = { - pass = 'R_SPF_ALLOW', - fail = 'R_SPF_FAIL', - softfail = 'R_SPF_SOFTFAIL', - neutral = 'R_SPF_NEUTRAL', - temperror = 'R_SPF_DNSFAIL', - none = 'R_SPF_NA', - permerror = 'R_SPF_PERMFAIL', - }, - dkim_symbols = { - pass = 'R_DKIM_ALLOW', - fail = 'R_DKIM_REJECT', - temperror = 'R_DKIM_TEMPFAIL', - none = 'R_DKIM_NA', - permerror = 'R_DKIM_PERMFAIL', - }, - dmarc_symbols = { - pass = 'DMARC_POLICY_ALLOW', - permerror = 'DMARC_BAD_POLICY', - temperror = 'DMARC_DNSFAIL', - none = 'DMARC_NA', - reject = 'DMARC_POLICY_REJECT', - softfail = 'DMARC_POLICY_SOFTFAIL', - quarantine = 'DMARC_POLICY_QUARANTINE', - }, - }, - }, -} - -local active_routines = {} -local custom_routines = {} - -local function milter_headers(task) - - if settings.skip_local then - local ip = task:get_ip() - if (ip and ip:is_local()) then return end - end - - if settings.skip_authenticated then - if task:get_user() ~= nil then return end - end - - local routines, common, add, remove = {}, {}, {}, {} - - routines['x-spamd-result'] = function() - if not common.symbols then - common.symbols = task:get_symbols_all() - common['metric_score'] = task:get_metric_score('default') - common['metric_action'] = task:get_metric_action('default') - end - if settings.routines['x-spamd-result'].remove then - remove[settings.routines['x-spamd-result'].header] = settings.routines['x-spamd-result'].remove - end - local buf = {} - table.insert(buf, table.concat({ - 'default: ', (common['metric_action'] == 'reject') and 'True' or 'False', ' [', - common['metric_score'][1], ' / ', common['metric_score'][2], ']' - })) - for _, s in ipairs(common.symbols) do - if not s.options then s.options = {} end - table.insert(buf, table.concat({ - ' ', s.name, ' (', s.score, ') [', table.concat(s.options, ','), ']', - })) - end - add[settings.routines['x-spamd-result'].header] = table.concat(buf, '\n') - end - - routines['x-rspamd-queue-id'] = function() - if common.queue_id ~= false then - common.queue_id = task:get_queue_id() - if not common.queue_id then - common.queue_id = false - end - end - if settings.routines['x-rspamd-queue-id'].remove then - remove[settings.routines['x-rspamd-queue-id'].header] = settings.routines['x-rspamd-queue-id'].remove - end - if common.queue_id then - add[settings.routines['x-rspamd-queue-id'].header] = common.queue_id - end - end - - routines['x-rspamd-server'] = function() - if settings.routines['x-rspamd-server'].remove then - remove[settings.routines['x-rspamd-server'].header] = settings.routines['x-rspamd-server'].remove - end - add[settings.routines['x-rspamd-server'].header] = HOSTNAME - end - - routines['x-spamd-bar'] = function() - if not common['metric_score'] then - common['metric_score'] = task:get_metric_score('default') - end - local score = common['metric_score'][1] - local spambar - if score <= -1 then - spambar = string.rep(settings.routines['x-spamd-bar'].negative, score*-1) - elseif score >= 1 then - spambar = string.rep(settings.routines['x-spamd-bar'].positive, score) - else - spambar = settings.routines['x-spamd-bar'].neutral - end - if settings.routines['x-spamd-bar'].remove then - remove[settings.routines['x-spamd-bar'].header] = settings.routines['x-spamd-bar'].remove - end - if spambar ~= '' then - add[settings.routines['x-spamd-bar'].header] = spambar - end - end - - routines['x-spam-level'] = function() - if not common['metric_score'] then - common['metric_score'] = task:get_metric_score('default') - end - local score = common['metric_score'][1] - if score < 1 then - return nil, {}, {} - end - if settings.routines['x-spam-level'].remove then - remove[settings.routines['x-spam-level'].header] = settings.routines['x-spam-level'].remove - end - add[settings.routines['x-spam-level'].header] = string.rep(settings.routines['x-spam-level'].char, score) - end - - routines['spam-header'] = function() - if not common['metric_action'] then - common['metric_action'] = task:get_metric_action('default') - end - if settings.routines['spam-header'].remove then - remove[settings.routines['spam-header'].header] = settings.routines['spam-header'].remove - end - local action = common['metric_action'] - if action ~= 'no action' and action ~= 'greylist' then - add[settings.routines['spam-header'].header] = settings.routines['spam-header'].value - end - end - - routines['x-virus'] = function() - if not common.symbols then - common.symbols = {} - end - if settings.routines['x-virus'].remove then - remove[settings.routines['x-virus'].header] = settings.routines['x-virus'].remove - end - local virii = {} - for _, sym in ipairs(settings.routines['x-virus'].symbols) do - if not (common.symbols[sym] == false) then - local s = task:get_symbol(sym) - if not s then - common.symbols[sym] = false - else - common.symbols[sym] = s - if (((s or E)[1] or E).options or E)[1] then - table.insert(virii, s[1].options[1]) - else - table.insert(virii, 'unknown') - end - end - end - end - if #virii > 0 then - add[settings.routines['x-virus'].header] = table.concat(virii, ',') - end - end - - routines['x-spam-status'] = function() - if not common['metric_score'] then - common['metric_score'] = task:get_metric_score('default') - end - if not common['metric_action'] then - common['metric_action'] = task:get_metric_action('default') - end - local score = common['metric_score'][1] - local action = common['metric_action'] - local is_spam - local spamstatus - if action ~= 'no action' and action ~= 'greylist' then - is_spam = 'Yes' - else - is_spam = 'No' - end - spamstatus = is_spam .. ', score=' .. string.format('%.2f', score) - if settings.routines['x-spam-status'].remove then - remove[settings.routines['x-spam-status'].header] = settings.routines['x-spam-status'].remove - end - add[settings.routines['x-spam-status'].header] = spamstatus - end - - routines['authentication-results'] = function() - local ar = require "auth_results" - - if settings.routines['authentication-results'].remove then - remove[settings.routines['authentication-results'].header] = - settings.routines['authentication-results'].remove - end - - local res = ar.gen_auth_results(task, - settings.routines['authentication-results']) - - if res then - add[settings.routines['authentication-results'].header] = res - end - end - - for _, n in ipairs(active_routines) do - local ok, err - if custom_routines[n] then - local to_add, to_remove, common_in - ok, err, to_add, to_remove, common_in = pcall(custom_routines[n], task, common) - if ok then - for k, v in pairs(to_add) do - add[k] = v - end - for k, v in pairs(to_remove) do - add[k] = v - end - for k, v in pairs(common_in) do - if type(v) == 'table' then - if not common[k] then - common[k] = {} - end - for kk, vv in pairs(v) do - common[k][kk] = vv - end - else - common[k] = v - end - end - end - else - ok, err = pcall(routines[n]) - end - if not ok then - logger.errx(task, 'call to %s failed: %s', n, err) - end - end - - if not next(add) then add = nil end - if not next(remove) then remove = nil end - if add or remove then - task:set_milter_reply({ - add_headers = add, - remove_headers = remove - }) - end -end - -local opts = rspamd_config:get_all_opt(N) or rspamd_config:get_all_opt('rmilter_headers') -if not opts then return end - -if type(opts['use']) == 'string' then - opts['use'] = {opts['use']} -elseif (type(opts['use']) == 'table' and not opts['use'][1]) then - logger.debugm(N, rspamd_config, 'no functions are enabled') - return -end -if type(opts['use']) ~= 'table' then - logger.errx(rspamd_config, 'unexpected type for "use" option: %s', type(opts['use'])) - return -end -if type(opts['custom']) == 'table' then - for k, v in pairs(opts['custom']) do - local f, err = load(v) - if not f then - logger.errx(rspamd_config, 'could not load "%s": %s', k, err) - else - custom_routines[k] = f() - end - end -end -local have_routine = {} -local function activate_routine(s) - if settings.routines[s] or custom_routines[s] then - have_routine[s] = true - table.insert(active_routines, s) - if (opts.routines and opts.routines[s]) then - for k, v in pairs(opts.routines[s]) do - settings.routines[s][k] = v - end - end - else - logger.errx(rspamd_config, 'routine "%s" does not exist', s) - end -end -if opts['extended_spam_headers'] then - activate_routine('x-spamd-result') - activate_routine('x-rspamd-server') - activate_routine('x-rspamd-queue-id') -end -if opts['skip_local'] then - settings.skip_local = true -end -if opts['skip_authenticated'] then - settings.skip_authenticated = true -end -for _, s in ipairs(opts['use']) do - if not have_routine[s] then - activate_routine(s) - end -end -if (#active_routines < 1) then - logger.errx(rspamd_config, 'no active routines') - return -end -logger.infox(rspamd_config, 'active routines [%s]', table.concat(active_routines, ',')) -rspamd_config:register_symbol({ - name = 'MILTER_HEADERS', - type = 'postfilter', - callback = milter_headers, - priority = 10 -}) diff --git a/data/Dockerfiles/rspamd/ratelimit.lua b/data/Dockerfiles/rspamd/ratelimit.lua new file mode 100644 index 00000000..e25ea42d --- /dev/null +++ b/data/Dockerfiles/rspamd/ratelimit.lua @@ -0,0 +1,723 @@ +--[[ +Copyright (c) 2011-2017, Vsevolod Stakhov +Copyright (c) 2016-2017, Andrew Lewis + +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 + +local E, settings = {}, {} +local N = 'ratelimit' +-- 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 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 limits_hash + +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 lua_redis = require "lua_redis" +local fun = require "fun" + +local user_keywords = {'user'} + +local redis_script_sha +local redis_script = [[local bucket +local limited = false +local buckets = {} +local queue_id = table.remove(ARGV) +local now = table.remove(ARGV) + +local argi = 0 +for i = 1, #KEYS do + local key = KEYS[i] + local period = tonumber(ARGV[argi+1]) + local limit = tonumber(ARGV[argi+2]) + if not buckets[key] then + buckets[key] = { + max_period = period, + limits = { {period, limit} }, + } + else + table.insert(buckets[key].limits, {period, limit}) + if period > buckets[key].max_period then + buckets[key].max_period = period + end + end + argi = argi + 2 +end + +for k, v in pairs(buckets) do + local maxp = v.max_period + redis.call('ZREMRANGEBYSCORE', k, '-inf', now - maxp) + for _, lim in ipairs(v.limits) do + local period = lim[1] + local limit = lim[2] + local rate + if period == maxp then + rate = redis.call('ZCARD', k) + else + rate = redis.call('ZCOUNT', k, now - period, '+inf') + end + if rate and rate >= limit then + limited = true + bucket = k + end + end + redis.call('EXPIRE', k, maxp) + if limited then break end +end + +if not limited then + for k in pairs(buckets) do + redis.call('ZADD', k, now, queue_id) + end +end + +return {limited, bucket}]] + +local redis_script_symbol = [[local limited = false +local buckets, results = {}, {} +local queue_id = table.remove(ARGV) +local now = table.remove(ARGV) + +local argi = 0 +for i = 1, #KEYS do + local key = KEYS[i] + local period = tonumber(ARGV[argi+1]) + local limit = tonumber(ARGV[argi+2]) + if not buckets[key] then + buckets[key] = { + max_period = period, + limits = { {period, limit} }, + } + else + table.insert(buckets[key].limits, {period, limit}) + if period > buckets[key].max_period then + buckets[key].max_period = period + end + end + argi = argi + 2 +end + +for k, v in pairs(buckets) do + local maxp = v.max_period + redis.call('ZREMRANGEBYSCORE', k, '-inf', now - maxp) + for _, lim in ipairs(v.limits) do + local period = lim[1] + local limit = lim[2] + local rate + if period == maxp then + rate = redis.call('ZCARD', k) + else + rate = redis.call('ZCOUNT', k, now - period, '+inf') + end + if rate then + local mult = 2 * math.tanh(rate / (limit * 2)) + if mult >= 0.5 then + table.insert(results, {k, tostring(mult)}) + end + end + end + redis.call('ZADD', k, now, queue_id) + redis.call('EXPIRE', k, maxp) +end + +return results]] + +local function load_scripts(cfg, ev_base) + local function rl_script_cb(err, data) + if err then + rspamd_logger.errx(cfg, 'Script loading failed: ' .. err) + elseif type(data) == 'string' then + redis_script_sha = data + end + end + local script + if ratelimit_symbol then + script = redis_script_symbol + else + script = redis_script + end + lua_redis.redis_make_request_taskless( + ev_base, + cfg, + redis_params, + nil, -- key + true, -- is write + rl_script_cb, --callback + 'SCRIPT', -- command + {'LOAD', script} + ) +end + +local limit_parser +local function parse_string_limit(lim, no_error) + 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[2], t[1] + end + + if not no_error then + rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim) + end + + return nil +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 string.lower(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, string.lower(r['addr'])) + table.insert(rate_keys, key_f) + total_rcpt = total_rcpt + 1 + end + end + return rate_keys + end +end + +local function process_buckets(task, buckets) + if not buckets then return end + local function rl_redis_cb(err, data) + if err then + rspamd_logger.infox(task, 'got error while setting limit: %1', err) + end + if not data then return end + if data[1] == 1 then + rspamd_logger.infox(task, + 'ratelimit "%s" exceeded', + data[2]) + task:set_pre_result('soft reject', + message_func(task, data[2])) + end + end + local function rl_symbol_redis_cb(err, data) + if err then + rspamd_logger.infox(task, 'got error while setting limit: %1', err) + end + if not data then return end + for i, b in ipairs(data) do + task:insert_result(ratelimit_symbol, b[2], string.format('%s:%s:%s', i, b[1], b[2])) + end + end + local redis_cb = rl_redis_cb + if ratelimit_symbol then redis_cb = rl_symbol_redis_cb end + local args = {redis_script_sha, #buckets} + for _, bucket in ipairs(buckets) do + table.insert(args, bucket[2]) + end + for _, bucket in ipairs(buckets) do + if use_ip_score then + local 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') + local key_keywords = rspamd_str_split(bucket[2], '_') + 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[1][2] = resize_element(asn_score, total_asn, bucket[1][2]) + elseif has_ip then + if total_ip and total_ip > ip_score_lower_bound then + bucket[1][2] = resize_element(ip_score, total_ip, bucket[1][2]) + elseif total_ipnet and total_ipnet > ip_score_lower_bound then + bucket[1][2] = resize_element(ipnet_score, total_ipnet, bucket[1][2]) + elseif total_asn and total_asn > ip_score_lower_bound then + bucket[1][2] = resize_element(asn_score, total_asn, bucket[1][2]) + elseif total_country and total_country > ip_score_lower_bound then + bucket[1][2] = resize_element(country_score, total_country, bucket[1][2]) + else + bucket[1][2] = resize_element(ip_score, total_ip, bucket[1][2]) + end + end + end + table.insert(args, bucket[1][1]) + table.insert(args, bucket[1][2]) + end + table.insert(args, rspamd_util.get_time()) + table.insert(args, task:get_queue_id() or task:get_uid()) + local ret = rspamd_redis_make_request(task, + redis_params, -- connect params + nil, -- hash key + true, -- is write + redis_cb, --callback + 'evalsha', -- command + args -- arguments + ) + if not ret then + rspamd_logger.errx(task, 'got error connecting to redis') + end +end + +local function ratelimit_cb(task) + if rspamd_lua_utils.is_rspamc_or_controller(task) then return end + 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) + fun.each(function(type) table.insert(rcpts_user, r[type]) end, {'user', 'addr'}) + end, rcpts) + if fun.any( + function(r) + if fun.any(function(w) return r == w end, whitelisted_rcpts) then return true end + 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 redis_keys = {} + local redis_keys_rev = {} + local function collect_redis_keys() + local function collect_cb(err, data) + if err then + rspamd_logger.errx(task, 'redis error: %1', err) + else + for i, d in ipairs(data) do + if type(d) == 'string' then + local plim, size = parse_string_limit(d) + if plim then + table.insert(args, {{plim, size}, redis_keys_rev[i]}) + end + end + end + return process_buckets(task, args) + end + end + local params, method + if limits_hash then + params = {limits_hash, rspamd_lua_utils.unpack(redis_keys)} + method = 'HMGET' + else + method = 'MGET' + params = redis_keys + end + local requested_keys = rspamd_redis_make_request(task, + redis_params, -- connect params + nil, -- hash key + true, -- is write + collect_cb, --callback + method, -- command + params -- arguments + ) + if not requested_keys then + rspamd_logger.errx(task, 'got error connecting to redis') + return process_buckets(task, args) + 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]) == '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) == 'string' then res = {res} end + for _, r in ipairs(res) do + local plim, size = parse_string_limit(r, true) + if plim then + table.insert(args, {{plim, size}, rk}) + else + local rkey = string.match(settings[k], 'redis:(.*)') + if rkey then + table.insert(redis_keys, rkey) + redis_keys_rev[#redis_keys] = rk + else + rspamd_logger.infox(task, "Don't know what to do with limit: %1", settings[k]) + end + end + end + end + end + else + if 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) == 'string' then res = {res} end + for _, r in ipairs(res) do + local plim, size = parse_string_limit(r, true) + if plim then + table.insert(args, {{plim, size}, rate_key}) + else + local rkey = string.match(r, 'redis:(.*)') + if rkey then + table.insert(redis_keys, rkey) + redis_keys_rev[#redis_keys] = rate_key + else + rspamd_logger.infox(task, "Don't know what to do with limit: %1", settings[k]) + end + end + end + elseif type(settings[k]) == 'table' then + for _, rl in ipairs(settings[k]) do + table.insert(args, {{rl[1], rl[2]}, rate_key}) + end + elseif type(settings[k]) == 'string' then + local rkey = string.match(settings[k], 'redis:(.*)') + if rkey then + table.insert(redis_keys, rkey) + redis_keys_rev[#redis_keys] = rate_key + else + rspamd_logger.infox(task, "Don't know what to do with limit: %1", settings[k]) + end + end + end + end + end + + if redis_keys[1] then + return collect_redis_keys() + else + return process_buckets(task, args) + end +end + +local opts = rspamd_config:get_all_opt(N) +if opts then + if opts['limit'] then + rspamd_logger.errx(rspamd_config, 'Legacy ratelimit config format no longer supported') + 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] = {} + fun.each(function(l) + local plim, size = parse_string_limit(l) + if plim then + table.insert(settings[t], {plim, size}) + end + end, lim) + elseif type(lim) == 'string' then + local plim, size = parse_string_limit(lim) + if plim then + settings[t] = { {plim, size} } + 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, 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['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 + + if opts['limits_hash'] then + limits_hash = opts['limits_hash'] + 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 + local s = { + type = 'prefilter,nostat', + name = 'RATELIMIT_CHECK', + priority = 4, + callback = ratelimit_cb, + } + if use_ip_score then + s.type = 'normal' + end + if ratelimit_symbol then + s.name = ratelimit_symbol + end + local id = rspamd_config:register_symbol(s) + if use_ip_score then + rspamd_config:register_dependency(id, 'IP_SCORE') + end + for _, v in pairs(custom_keywords) do + if type(v) == 'table' and type(v['init']) == 'function' then + v['init']() + end + end + end +end +rspamd_config:add_on_load(function(cfg, ev_base, worker) + load_scripts(cfg, ev_base) +end) diff --git a/data/Dockerfiles/rspamd/settings.conf b/data/Dockerfiles/rspamd/settings.conf index 4449f091..3c1c9c16 100644 --- a/data/Dockerfiles/rspamd/settings.conf +++ b/data/Dockerfiles/rspamd/settings.conf @@ -1 +1 @@ -settings = "http://nginx:8081/settings.php"; +settings = "http://172.22.1.251:8081/settings.php"; diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 0fc4675f..a9cefc47 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -17,6 +17,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ syslog-ng-core \ syslog-ng-mod-redis \ dirmngr \ + netcat \ + psmisc \ wget \ && rm -rf /var/lib/apt/lists/* \ && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ @@ -37,7 +39,7 @@ RUN mkdir /usr/share/doc/sogo \ && echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds' >> /etc/cron.d/sogo \ && touch /etc/default/locale -COPY ./reconf-domains.sh / +COPY ./bootstrap-sogo.sh / COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY supervisord.conf /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/sogo/reconf-domains.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh similarity index 96% rename from data/Dockerfiles/sogo/reconf-domains.sh rename to data/Dockerfiles/sogo/bootstrap-sogo.sh index ec0d4c93..92acb591 100755 --- a/data/Dockerfiles/sogo/reconf-domains.sh +++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -3,6 +3,13 @@ # Wait for MySQL to warm-up while mysqladmin ping --host 172.22.1.250 --silent; do +# Wait until port becomes free and send sig +until ! nc -z sogo-mailcow 20000; +do + killall -TERM sogod + sleep 3 +done + # Recreate view mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view" @@ -93,8 +100,6 @@ echo ' chown sogo:sogo -R /var/lib/sogo/ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist -supervisorctl restart sogo - -sleep 99999 +exec gosu sogo /usr/sbin/sogod done diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index 30392e3b..4c611a39 100644 --- a/data/Dockerfiles/sogo/supervisord.conf +++ b/data/Dockerfiles/sogo/supervisord.conf @@ -22,25 +22,16 @@ user=sogo autorestart=true priority=4 -[program:reconf-domains] -command=/reconf-domains.sh +[program:bootstrap-sogo] +command=/bootstrap-sogo.sh stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 priority=3 +startretries=10 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 +stopwaitsecs=120 [inet_http_server] port=9191 diff --git a/data/Dockerfiles/sogo/syslog-ng.conf b/data/Dockerfiles/sogo/syslog-ng.conf index 264bca44..51628353 100644 --- a/data/Dockerfiles/sogo/syslog-ng.conf +++ b/data/Dockerfiles/sogo/syslog-ng.conf @@ -33,10 +33,19 @@ destination d_redis_f2b_channel { command("PUBLISH" "F2B_CHANNEL" "$MESSAGE") ); }; +destination d_redis_cleanup { + redis( + host("redis-mailcow") + persist-name("redis3") + port(6379) + command("LTRIM" "SOGO_LOG" "0" "9999") + ); +}; log { source(s_sogo); destination(d_redis_ui_log); destination(d_redis_f2b_channel); + destination(d_redis_cleanup); }; log { source(s_sogo); diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 2d416d21..e478f0b2 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -13,7 +13,7 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib #mail_crypt +mail_plugins = quota acl zlib listescape #mail_crypt ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA @@ -214,10 +214,10 @@ userdb { driver = sql } protocol imap { - mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve #mail_crypt + mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape #mail_crypt } protocol lmtp { - mail_plugins = quota sieve acl zlib #mail_crypt + mail_plugins = quota sieve acl zlib listescape #mail_crypt auth_socket_path = /usr/local/var/run/dovecot/auth-master } protocol sieve { @@ -248,6 +248,7 @@ plugin { sieve_max_script_size = 1M sieve_quota_max_scripts = 0 sieve_quota_max_storage = 0 + listescape_char = "\\" #mail_crypt_global_private_key = prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :object"); - $stmt->execute(array(':object' => $row['object'])); + $stmt->execute(array(':object' => $object)); $alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC); array_filter($alias_domains); while ($row = array_shift($alias_domains)) { - $rcpt[] = '/.*@' . $row['alias_domain'] . '/'; + $rcpt[] = '/.*@' . $row['alias_domain'] . '/i'; } } if (!empty($rcpt)) { @@ -112,7 +112,7 @@ while ($row = array_shift($rows)) { score_ { priority = 4; rcpt = ""; fetchAll(PDO::FETCH_COLUMN); $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0]))); ?> - from = "/()/"; + from = "/()/i"; priority = 5; rcpt = ""; priority = 6; + rcpt = ""; + + apply "default" { + MAILCOW_WHITE = -999.0; + } + symbols [ + "MAILCOW_WHITE" + ] + } + whitelist_header_ { +prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf` + WHERE `object`= :object + AND `option` = 'whitelist_from'"); + $stmt->execute(array(':object' => $row['object'])); + $grouped_lists = $stmt->fetchAll(PDO::FETCH_COLUMN); + $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0]))); +?> + request_header = { + "From" = "()"; + } + + priority = 5; + + rcpt = ""; + + priority = 6; + rcpt = ""; fetchAll(PDO::FETCH_COLUMN); $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0]))); ?> - from = "/()/"; + from = "/()/i"; priority = 5; rcpt = ""; priority = 6; + rcpt = ""; + + apply "default" { + MAILCOW_BLACK = 999.0; + } + symbols [ + "MAILCOW_BLACK" + ] + } + blacklist_header_ { +prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf` + WHERE `object`= :object + AND `option` = 'blacklist_from'"); + $stmt->execute(array(':object' => $row['object'])); + $grouped_lists = $stmt->fetchAll(PDO::FETCH_COLUMN); + $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0]))); +?> + request_header = { + "From" = "()"; + } + + priority = 5; + + rcpt = ""; + + priority = 6; + rcpt = "";
- +
@@ -121,6 +121,18 @@ $tfa_data = get_tfa();
+
+ +
+
@@ -253,7 +265,8 @@ XYZ
- + +
@@ -291,6 +304,7 @@ XYZ
+
@@ -318,6 +332,48 @@ XYZ
+ + +
+
Relayhosts
+
+

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

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
diff --git a/data/web/autoconfig.php b/data/web/autoconfig.php index d01bc724..c905c325 100644 --- a/data/web/autoconfig.php +++ b/data/web/autoconfig.php @@ -1,66 +1,91 @@ '; ?> - + %EMAILDOMAIN% A mailcow mail server mail server - - 993 + + SSL %EMAILADDRESS% password-cleartext - - 143 + + STARTTLS %EMAILADDRESS% password-cleartext + - - 995 + + SSL %EMAILADDRESS% password-cleartext + + - - 110 + + STARTTLS %EMAILADDRESS% password-cleartext + - - 465 + + SSL %EMAILADDRESS% password-cleartext - - - 587 + + STARTTLS %EMAILADDRESS% password-cleartext - + If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden. @@ -68,6 +93,6 @@ header('Content-Type: application/xml'); - + diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index 061acb61..975fd3e0 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -16,13 +16,14 @@ error_reporting(0); $data = trim(file_get_contents("php://input")); // Desktop client needs IMAP, unless it's Outlook 2013 or higher on Windows -if (strpos($data, 'autodiscover/outlook/responseschema')) { // desktop client +if (strpos($data, 'autodiscover/outlook/responseschema') !== false) { // desktop client $configuration['autodiscoverType'] = 'imap'; if ($configuration['useEASforOutlook'] == 'yes' && - // Office for macOS does not support EAS - strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && - // Outlook 2013 (version 15) or higher - preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT'])) { + // Office for macOS does not support EAS + strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && + // Outlook 2013 (version 15) or higher + preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT']) + ) { $configuration['autodiscoverType'] = 'activesync'; } } @@ -64,8 +65,28 @@ else { Request->EMailAddress; + try { + $discover = new SimpleXMLElement($data); + $email = $discover->Request->EMailAddress; + } catch (Exception $e) { + $email = $_SERVER['PHP_AUTH_USER']; + } + + $username = trim($email); + try { + $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + die("Failed to determine name from SQL"); + } + if (!empty($MailboxData['name'])) { + $displayname = utf8_encode($MailboxData['name']); + } + else { + $displayname = $email; + } if ($configuration['autodiscoverType'] == 'imap') { ?> @@ -100,13 +121,13 @@ else { CalDAV - /SOGo/dav//Calendar + https:///SOGo/dav//Calendar off CardDAV - /SOGo/dav//Contacts + https:///SOGo/dav//Contacts off @@ -115,21 +136,6 @@ else { prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); - } - else { - $displayname = $email; - } ?> en:en diff --git a/data/web/css/admin.css b/data/web/css/admin.css index 7f8897a0..aa07d7a3 100644 --- a/data/web/css/admin.css +++ b/data/web/css/admin.css @@ -41,4 +41,15 @@ body.modal-open { -moz-transform:rotateX(180deg); -webkit-transform:rotateX(180deg); transform:rotateX(180deg); -} \ No newline at end of file +} +.anchor { + display: block; + height: 65px; + margin-top: -65px; + visibility: hidden; +} +.scrollboxFixed { + position: fixed; + top: 65px; + z-index: 1; +} diff --git a/data/web/css/edit.css b/data/web/css/edit.css index fe4d9fff..07d4e745 100644 --- a/data/web/css/edit.css +++ b/data/web/css/edit.css @@ -27,3 +27,6 @@ table.footable>tbody>tr.footable-empty>td { user-select: none; padding:10px 0 10px 0; } +.inputMissingAttr { + border-color: #FF4136; +} diff --git a/data/web/edit.php b/data/web/edit.php index d156dba0..0e7460bc 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -16,594 +16,685 @@ require_once("inc/header.inc.php");
-

-
-
"> - - -
- -
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- - - -

-
-
"> - - -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- - - -

-
"> - - - - -
- -
- -
-
- -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
- -
- -

-
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-

Domain: (._domainkey)

-
-
-
-
-
- -
-
-
-

-

-
-
-
-
-
- - - -
-
-
-
- - - - -
-
-
-
-

-

-
-
-
-
-
- - - -
-
-
-
- - - - -
-
-
-
+if (isset($_SESSION['mailcow_cc_role'])) { + if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") { + if (isset($_GET["alias"]) && + !empty($_GET["alias"])) { + $alias = $_GET["alias"]; + $result = mailbox('get', 'alias_details', $alias); + if (!empty($result)) { + ?> +

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

+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
- - -

-
"> - - -
- -
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-

Domain: (._domainkey)

-
-
-
-
-
- - - -

-
"> - - - -
- -
- -
-
-
- -
- -
-
-
- -
- + + +
+ +
+ +
+
- - - - - - - - - - - - - - - -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +

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

Domain: (._domainkey)

+
+
+
+ +
+
+
+ +
-
- -
+ +
+
+
- +
+
+

+

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

+

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

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

Domain: (._domainkey)

+
+
+
+
+
+ + + -

-
"> +

+ + - - -
- -
- -
-
-
- -
- -
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- - - - - -

-
"> - - - - -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- - - - - + +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ + +

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

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

+
+ + + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + diff --git a/data/web/inc/call_sogo_ctrl.php b/data/web/inc/call_sogo_ctrl.php index 7c7cff9e..990defa2 100644 --- a/data/web/inc/call_sogo_ctrl.php +++ b/data/web/inc/call_sogo_ctrl.php @@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role exit(); } if ($_GET['ACTION'] == "start") { - $request = xmlrpc_encode_request("supervisor.startProcess", 'reconf-domains', array('encoding'=>'utf-8')); + $request = xmlrpc_encode_request("supervisor.startProcess", 'bootstrap-sogo', array('encoding'=>'utf-8')); $context = stream_context_create(array('http' => array( 'method' => "POST", 'header' => "Content-Length: " . strlen($request), @@ -22,7 +22,7 @@ if ($_GET['ACTION'] == "start") { } } elseif ($_GET['ACTION'] == "stop") { - $request = xmlrpc_encode_request("supervisor.stopProcess", 'sogo', array('encoding'=>'utf-8')); + $request = xmlrpc_encode_request("supervisor.stopProcess", 'bootstrap-sogo', array('encoding'=>'utf-8')); $context = stream_context_create(array('http' => array( 'method' => "POST", 'header' => "Content-Length: " . strlen($request), @@ -34,21 +34,7 @@ elseif ($_GET['ACTION'] == "stop") { echo '' . $response['faultString'] . ''; } else { - sleep(4); - $request = xmlrpc_encode_request("supervisor.stopProcess", 'reconf-domains', array('encoding'=>'utf-8')); - $context = stream_context_create(array('http' => array( - 'method' => "POST", - 'header' => "Content-Length: " . strlen($request), - 'content' => $request - ))); - $file = @file_get_contents("http://sogo:9191/RPC2", false, $context) or die("Cannot connect to $remote_server:$listener_port"); - $response = xmlrpc_decode($file); - if (isset($response['faultString'])) { - echo '' . $response['faultString'] . ''; - } - else { echo 'OK'; - } } } -?> \ No newline at end of file +?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 8daaff79..55a8f88d 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -18,7 +18,8 @@ function setLang(sel) { $(document).ready(function() { function mailcow_alert_box(message, type) { - $.notify({message: message},{type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}}); + msg = $('').html(message).text(); + $.notify({message: msg},{type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}}); } mailcow_alert_box("", ""); diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php new file mode 100644 index 00000000..aa434079 --- /dev/null +++ b/data/web/inc/functions.domain_admin.inc.php @@ -0,0 +1,507 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + if (empty($domains)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `admin` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + foreach ($num_results as $num_results_each) { + if ($num_results_each != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username)) + ); + return false; + } + } + if (!empty($password) && !empty($password2)) { + if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + if ($password != $password2) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + $password_hashed = hash_password($password); + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username, :domain, :created, :active)"); + $stmt->execute(array( + ':username' => $username, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + catch (PDOException $e) { + domain_admin('delete', $username); + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + try { + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); + $stmt->execute(array( + ':username' => $username, + ':password_hashed' => $password_hashed, + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_empty']) + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username)) + ); + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + // Administrator + if ($_SESSION['mailcow_cc_role'] == "admin") { + if (!is_array($_data['username'])) { + $usernames = array(); + $usernames[] = $_data['username']; + } + else { + $usernames = $_data['username']; + } + foreach ($usernames as $username) { + $is_now = domain_admin('details', $username); + $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; + $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $password = $_data['password']; + $password2 = $_data['password2']; + + if (!empty($domains)) { + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + } + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + if ($username_new != $username) { + if (!empty(domain_admin('details', $username_new)['username'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + } + try { + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + + if (!empty($domains)) { + foreach ($domains as $domain) { + try { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username_new, :domain, :created, :active)"); + $stmt->execute(array( + ':username_new' => $username_new, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + } + + if (!empty($password) && !empty($password2)) { + if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + if ($password != $password2) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + $password_hashed = hash_password($password); + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + else { + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars(implode(', ', $usernames))) + ); + } + // Domain administrator + // Can only edit itself + elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { + $username = $_SESSION['mailcow_cc_username']; + $password_old = $_data['user_old_pass']; + $password_new = $_data['user_new_pass']; + $password_new2 = $_data['user_new_pass2']; + + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :user"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!verify_ssha256($row['password'], $password_old)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + + if (!empty($password_new2) && !empty($password_new)) { + if ($password_new2 != $password_new) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + $password_hashed = hash_password($password_new); + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username' => $username + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username)) + ); + } + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $usernames = (array)$_data['username']; + foreach ($usernames as $username) { + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars(implode(', ', $usernames))) + ); + break; + case 'get': + $domainadmins = array(); + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $stmt = $pdo->query("SELECT DISTINCT + `username` + FROM `domain_admins` + WHERE `username` IN ( + SELECT `username` FROM `admin` + WHERE `superadmin`!='1' + )"); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $domainadmins[] = $row['username']; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $domainadmins; + break; + case 'details': + $domainadmindata = array(); + + if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { + return false; + } + elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { + return false; + } + try { + $stmt = $pdo->prepare("SELECT + `tfa`.`active` AS `tfa_active_int`, + CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, + `domain_admins`.`username`, + `domain_admins`.`created`, + `domain_admins`.`active` AS `active_int`, + CASE `domain_admins`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` + FROM `domain_admins` + LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` + WHERE `domain_admins`.`username`= :domain_admin"); + $stmt->execute(array( + ':domain_admin' => $_data + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = $row['tfa_active']; + $domainadmindata['active'] = $row['active']; + $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $row['created']; + // GET SELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['selected_domains'][] = $row['domain']; + } + // GET UNSELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` NOT IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['unselected_domains'][] = $row['domain']; + } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $domainadmindata; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index c9644d5f..44e4e5b4 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -17,7 +17,12 @@ function fail2ban($_action, $_data = null) { foreach ($wl as $key => $value) { $tmp_data[] = $key; } - $data['whitelist'] = implode(PHP_EOL, $tmp_data); + if (isset($tmp_data)) { + $data['whitelist'] = implode(PHP_EOL, $tmp_data); + } + else { + $data['whitelist'] = ""; + } } else { $data['whitelist'] = ""; diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index b3420530..79f3c6dc 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -62,18 +62,17 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } +function pem_to_der($pem_key) { + // Need to remove BEGIN/END PUBLIC KEY + $lines = explode("\n", trim($pem_key)); + unset($lines[count($lines)-1]); + unset($lines[0]); + return base64_decode(implode('', $lines)); +} function generate_tlsa_digest($hostname, $port, $starttls = null) { if (!is_valid_domain_name($hostname)) { return "Not a valid hostname"; } - function pem_to_der($pem_key) { - // Need to remove BEGIN/END PUBLIC KEY - $lines = explode("\n", trim($pem_key)); - unset($lines[count($lines)-1]); - unset($lines[0]); - return base64_decode(implode('', $lines)); - } - if (empty($starttls)) { $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); $stream = stream_socket_client('tls://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context); @@ -88,20 +87,24 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { return $error_nr . ': ' . $error_msg; } $banner = fread($stream, 512 ); - if (preg_match("/^220/i", $banner)) { + if (preg_match("/^220/i", $banner)) { // SMTP fwrite($stream,"HELO tlsa.generator.local\r\n"); fread($stream, 512); fwrite($stream,"STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/imap.+starttls/i", $banner)) { + elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP fwrite($stream,"A1 STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/^\+OK/", $banner)) { + elseif (preg_match("/^\+OK/", $banner)) { // POP3 fwrite($stream,"STLS\r\n"); fread($stream, 512); } + elseif (preg_match("/^OK/m", $banner)) { // Sieve + fwrite($stream,"STARTTLS\r\n"); + fread($stream, 512); + } else { return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"'; } @@ -113,7 +116,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 +144,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; @@ -240,6 +218,32 @@ function check_login($user, $pass) { } sleep($_SESSION['ldelay']); } +function set_acl() { + global $pdo; + if (!isset($_SESSION['mailcow_cc_username'])) { + return false; + } + if ($_SESSION['mailcow_cc_role'] == 'admin' || $_SESSION['mailcow_cc_role'] == 'domainadmin') { + $stmt = $pdo->query("SHOW COLUMNS FROM `user_acl` WHERE `Field` != 'username';"); + $acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($acl_all)) { + $acl['acl'][$row['Field']] = 1; + } + } + else { + $username = strtolower(trim($_SESSION['mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $acl['acl'] = $stmt->fetch(PDO::FETCH_ASSOC); + unset($acl['acl']['username']); + } + if (!empty($acl)) { + $_SESSION = array_merge($_SESSION, $acl); + } + else { + return false; + } +} function formatBytes($size, $precision = 2) { if(!is_numeric($size)) { return "0"; @@ -272,7 +276,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 +351,20 @@ function edit_admin_account($postarray) { function edit_user_account($postarray) { global $lang; global $pdo; - if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - else { - $username = $postarray['username']; - } + $username = $_SESSION['mailcow_cc_username']; + $role = $_SESSION['mailcow_cc_role']; + $password_old = $postarray['user_old_pass']; + if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; } - else { - $username = $_SESSION['mailcow_cc_username']; - } - $password_old = $postarray['user_old_pass']; - if (isset($postarray['user_new_pass']) && isset($postarray['user_new_pass2'])) { $password_new = $postarray['user_new_pass']; $password_new2 = $postarray['user_new_pass2']; } - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `username` = :user"); @@ -382,7 +377,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 +480,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 +560,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 +583,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 +611,6 @@ function set_tfa($postarray) { ); } break; - case "none": try { $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); @@ -977,7 +687,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 +750,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 +834,6 @@ function verify_tfa_login($username, $token) { } return false; } -function edit_domain_admin($postarray) { - global $lang; - global $pdo; - - if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - // Administrator - if ($_SESSION['mailcow_cc_role'] == "admin") { - if (!is_array($postarray['username'])) { - $usernames = array(); - $usernames[] = $postarray['username']; - } - else { - $usernames = $postarray['username']; - } - foreach ($usernames as $username) { - $is_now = get_domain_admin_details($username); - $domains = (isset($postarray['domains'])) ? (array)$postarray['domains'] : null; - if (!empty($is_now)) { - $active = (isset($postarray['active'])) ? $postarray['active'] : $is_now['active_int']; - $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; - $username_new = (!empty($postarray['username_new'])) ? $postarray['username_new'] : $is_now['username']; - } - else { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - $password = $postarray['password']; - $password2 = $postarray['password2']; - - if (!empty($domains)) { - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['domain_invalid']) - ); - return false; - } - } - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - if ($username_new != $username) { - if (!empty(get_domain_admin_details($username_new)['username'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - } - try { - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - - if (!empty($domains)) { - foreach ($domains as $domain) { - try { - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username_new, :domain, :created, :active)"); - $stmt->execute(array( - ':username_new' => $username_new, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - } - - if (!empty($password) && !empty($password2)) { - if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - if ($password != $password2) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_mismatch']) - ); - return false; - } - $password_hashed = hash_password($password); - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($postarray['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - else { - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($postarray['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars(implode(', ', $usernames))) - ); - } - // Domain administrator - // Can only edit itself - elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { - $username = $_SESSION['mailcow_cc_username']; - $password_old = $postarray['user_old_pass']; - $password_new = $postarray['user_new_pass']; - $password_new2 = $postarray['user_new_pass2']; - - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :user"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!verify_ssha256($row['password'], $password_old)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - - if (!empty($password_new2) && !empty($password_new)) { - if ($password_new2 != $password_new) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_mismatch']) - ); - return false; - } - if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - $password_hashed = hash_password($password_new); - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username' => $username - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username)) - ); - } -} function get_admin_details() { // No parameter to be given, only one admin should exist global $pdo; @@ -1438,4 +915,4 @@ function get_logs($container, $lines = 100) { } return false; } -?> +?> \ No newline at end of file diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 597d9d87..4baca2b7 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -7,6 +7,13 @@ function mailbox($_action, $_type, $_data = null) { case 'add': switch ($_type) { case 'time_limited_alias': + if (!isset($_SESSION['acl']['spam_alias']) || $_SESSION['acl']['spam_alias'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) { $_SESSION['return'] = array( @@ -66,6 +73,13 @@ function mailbox($_action, $_type, $_data = null) { ); break; case 'syncjob': + if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) { $_SESSION['return'] = array( @@ -78,14 +92,22 @@ function mailbox($_action, $_type, $_data = null) { $username = $_data['username']; } } - else { + elseif ($_SESSION['mailcow_cc_role'] == "user") { $username = $_SESSION['mailcow_cc_username']; } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'No user defined' + ); + return false; + } $active = intval($_data['active']); $delete2duplicates = intval($_data['delete2duplicates']); $delete1 = intval($_data['delete1']); + $delete2 = intval($_data['delete2']); $port1 = $_data['port1']; - $host1 = $_data['host1']; + $host1 = strtolower($_data['host1']); $password1 = $_data['password1']; $exclude = $_data['exclude']; $maxage = $_data['maxage']; @@ -135,9 +157,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) { @@ -155,13 +177,14 @@ function mailbox($_action, $_type, $_data = null) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) - VALUES (:user2, :exclude, :maxage, :delete1, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); + $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) + VALUES (:user2, :exclude, :maxage, :delete1, :delete2, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); $stmt->execute(array( ':user2' => $username, ':exclude' => $exclude, ':maxage' => $maxage, ':delete1' => $delete1, + ':delete2' => $delete2, ':subfolder2' => $subfolder2, ':host1' => $host1, ':authmech1' => 'PLAIN', @@ -260,8 +283,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, @@ -736,6 +759,10 @@ function mailbox($_action, $_type, $_data = null) { ':domain' => $domain, ':active' => $active )); + $stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`) VALUES (:username)"); + $stmt->execute(array( + ':username' => $username + )); $_SESSION['return'] = array( 'type' => 'success', 'msg' => sprintf($lang['success']['mailbox_added'], htmlspecialchars($username)) @@ -879,7 +906,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 +930,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']) @@ -942,6 +969,13 @@ function mailbox($_action, $_type, $_data = null) { else { $usernames = $_data['username']; } + if (!isset($_SESSION['acl']['tls_policy']) || $_SESSION['acl']['tls_policy'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($usernames as $username) { if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) { $_SESSION['return'] = array( @@ -950,12 +984,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( @@ -993,6 +1025,13 @@ function mailbox($_action, $_type, $_data = null) { else { $usernames = $_data['username']; } + if (!isset($_SESSION['acl']['spam_score']) || $_SESSION['acl']['spam_score'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($usernames as $username) { $lowspamlevel = explode(',', $_data['spam_score'])[0]; $highspamlevel = explode(',', $_data['spam_score'])[1]; @@ -1041,6 +1080,13 @@ function mailbox($_action, $_type, $_data = null) { ); break; case 'time_limited_alias': + if (!isset($_SESSION['acl']['spam_alias']) || $_SESSION['acl']['spam_alias'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } if (!is_array($_data['address'])) { $addresses = array(); $addresses[] = $_data['address']; @@ -1098,6 +1144,13 @@ function mailbox($_action, $_type, $_data = null) { else { $usernames = $_data['username']; } + if (!isset($_SESSION['acl']['delimiter_action']) || $_SESSION['acl']['delimiter_action'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($usernames as $username) { if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) { $_SESSION['return'] = array( @@ -1136,6 +1189,61 @@ 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_VALUE', $domain); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + } + else { + try { + $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(); @@ -1144,14 +1252,22 @@ function mailbox($_action, $_type, $_data = null) { else { $ids = $_data['id']; } + if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($ids as $id) { $is_now = mailbox('get', 'syncjob_details', $id); 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']; + $delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2']; $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']; @@ -1210,10 +1326,11 @@ function mailbox($_action, $_type, $_data = null) { return false; } try { - $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active + $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, `delete2` = :delete2, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active WHERE `id` = :id"); $stmt->execute(array( ':delete1' => $delete1, + ':delete2' => $delete2, ':id' => $id, ':exclude' => $exclude, ':maxage' => $maxage, @@ -1253,7 +1370,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 +1500,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 +1594,7 @@ function mailbox($_action, $_type, $_data = null) { `active` = :active, `quota` = :quota, `maxquota` = :maxquota, + `relayhost` = :relayhost, `mailboxes` = :mailboxes, `aliases` = :aliases, `description` = :description @@ -1486,6 +1605,7 @@ function mailbox($_action, $_type, $_data = null) { ':active' => $active, ':quota' => $quota, ':maxquota' => $maxquota, + ':relayhost' => $relayhost, ':mailboxes' => $mailboxes, ':aliases' => $aliases, ':description' => $description, @@ -1524,7 +1644,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 +1708,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 +1724,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 +1877,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']; } @@ -2006,7 +2122,11 @@ function mailbox($_action, $_type, $_data = null) { return false; } try { - $stmt = $pdo->prepare("SELECT * FROM `imapsync` WHERE id = :id"); + $stmt = $pdo->prepare("SELECT *, + CONCAT(LEFT(`password1`, 3), '...') AS `password1_short`, + `active` AS `active_int`, + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` + FROM `imapsync` WHERE id = :id"); $stmt->execute(array(':id' => $_data)); $syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC); } @@ -2032,14 +2152,12 @@ function mailbox($_action, $_type, $_data = null) { $_data = $_SESSION['mailcow_cc_username']; } try { - $stmt = $pdo->prepare("SELECT *, - CONCAT(LEFT(`password1`, 3), '...') AS `password1_short`, - `active` AS `active_int`, - CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` - FROM `imapsync` - WHERE `user2` = :username"); + $stmt = $pdo->prepare("SELECT `id` FROM `imapsync` WHERE `user2` = :username"); $stmt->execute(array(':username' => $_data)); - $syncjobdata = $stmt->fetchAll(PDO::FETCH_ASSOC); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $syncjobdata[] = $row['id']; + } } catch(PDOException $e) { $_SESSION['return'] = array( @@ -2267,6 +2385,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)) { + $rl = explode(' / 1', $rl_value); + $data['value'] = $rl[0]; + $data['frame'] = $rl[1]; + return $data; + } + else { + return false; + } + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + return false; + break; case 'alias_details': $aliasdata = array(); try { @@ -2394,7 +2537,7 @@ function mailbox($_action, $_type, $_data = null) { ':domain' => $_data )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($row)) { + if (!empty($row)) { $_data = $row['target_domain']; } $stmt = $pdo->prepare("SELECT @@ -2404,6 +2547,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 +2582,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']; @@ -2587,6 +2732,13 @@ function mailbox($_action, $_type, $_data = null) { else { $ids = $_data['id']; } + if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($ids as $id) { if (!is_numeric($id)) { $_SESSION['return'] = array( @@ -2631,6 +2783,13 @@ function mailbox($_action, $_type, $_data = null) { else { $addresses = $_data['address']; } + if (!isset($_SESSION['acl']['spam_alias']) || $_SESSION['acl']['spam_alias'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($addresses as $address) { try { $stmt = $pdo->prepare("SELECT `goto` FROM `spamalias` WHERE `address` = :address"); @@ -2679,6 +2838,13 @@ function mailbox($_action, $_type, $_data = null) { else { $usernames = $_data['username']; } + if (!isset($_SESSION['acl']['eas_reset']) || $_SESSION['acl']['eas_reset'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($usernames as $username) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) { $_SESSION['return'] = array( diff --git a/data/web/inc/functions.policy.inc.php b/data/web/inc/functions.policy.inc.php index 3070b5dc..2de1c6e2 100644 --- a/data/web/inc/functions.policy.inc.php +++ b/data/web/inc/functions.policy.inc.php @@ -1,5 +1,4 @@ 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } if ($_data['object_list'] == "bl") { $object_list = "blacklist_from"; } @@ -233,6 +239,13 @@ function policy($_action, $_scope, $_data = null) { else { $prefids = $_data['prefid']; } + if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } foreach ($prefids as $prefid) { if (!is_numeric($prefid)) { $_SESSION['return'] = array( diff --git a/data/web/inc/functions.relayhost.inc.php b/data/web/inc/functions.relayhost.inc.php new file mode 100644 index 00000000..249dacc3 --- /dev/null +++ b/data/web/inc/functions.relayhost.inc.php @@ -0,0 +1,179 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $hostname = trim($_data['hostname']); + $username = str_replace(':', '\:', trim($_data['username'])); + $password = str_replace(':', '\:', trim($_data['password'])); + if (empty($hostname)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid host specified: '. htmlspecialchars($host) + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `relayhosts` (`hostname`, `username` ,`password`, `active`) + VALUES (:hostname, :username, :password, :active)"); + $stmt->execute(array( + ':hostname' => $hostname, + ':username' => $username, + ':password' => $password, + ':active' => '1' + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['relayhost_added'], htmlspecialchars(implode(', ', $hosts))) + ); + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $ids = (array)$_data['id']; + foreach ($ids as $id) { + $is_now = relayhost('details', $id); + if (!empty($is_now)) { + $hostname = (!empty($_data['hostname'])) ? trim($_data['hostname']) : $is_now['hostname']; + $username = (!empty($_data['username'])) ? trim($_data['username']) : $is_now['username']; + $password = (!empty($_data['password'])) ? trim($_data['password']) : $is_now['password']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Relayhost invalid' + ); + return false; + } + try { + $stmt = $pdo->prepare("UPDATE `relayhosts` SET + `hostname` = :hostname, + `username` = :username, + `password` = :password, + `active` = :active + WHERE `id` = :id"); + $stmt->execute(array( + ':id' => $id, + ':hostname' => $hostname, + ':username' => $username, + ':password' => $password, + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars(implode(', ', $hostnames))) + ); + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $ids = (array)$_data['id']; + foreach ($ids as $id) { + try { + $stmt = $pdo->prepare("DELETE FROM `relayhosts` WHERE `id`= :id"); + $stmt->execute(array(':id' => $id)); + $stmt = $pdo->prepare("UPDATE `domain` SET `relayhost` = '0' WHERE `relayhost`= :id"); + $stmt->execute(array(':id' => $id)); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['relayhost_removed'], htmlspecialchars(implode(', ', $hostnames))) + ); + break; + case 'get': + if ($_SESSION['mailcow_cc_role'] != "admin") { + return false; + } + $relayhosts = array(); + try { + $stmt = $pdo->query("SELECT `id`, `hostname`, `username` FROM `relayhosts`"); + $relayhosts = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $relayhosts; + break; + case 'details': + if ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + $relayhostdata = array(); + try { + $stmt = $pdo->prepare("SELECT `id`, + `hostname`, + `username`, + `password`, + `active` AS `active_int`, + CONCAT(LEFT(`password`, 3), '...') AS `password_short`, + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` + FROM `relayhosts` + WHERE `id` = :id"); + $stmt->execute(array(':id' => $_data)); + $relayhostdata = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!empty($relayhostdata)) { + $stmt = $pdo->prepare("SELECT GROUP_CONCAT(`domain` SEPARATOR ', ') AS `used_by_domains` FROM `domain` WHERE `relayhost` = :id"); + $stmt->execute(array(':id' => $_data)); + $used_by_domains = $stmt->fetch(PDO::FETCH_ASSOC)['used_by_domains']; + $used_by_domains = (empty($used_by_domains)) ? '' : $used_by_domains; + $relayhostdata['used_by_domains'] = $used_by_domains; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $relayhostdata; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index 533711de..98d42ef1 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -29,7 +29,7 @@ - +