diff --git a/.gitignore b/.gitignore index 6ccd199d..c61097e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ data/conf/nginx/server_name.active data/conf/postfix/sql data/conf/dovecot/sql data/web/inc/vars.local.inc.php -site/ data/assets/ssl diff --git a/README.md b/README.md index 025dc825..29857408 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -Please see [the official documentation](https://andryyy.github.io/mailcow-dockerized/) for instructions. +Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. diff --git a/data/Dockerfiles/clamav/Dockerfile b/data/Dockerfiles/clamav/Dockerfile index 5fc44d9a..170b7d8f 100755 --- a/data/Dockerfiles/clamav/Dockerfile +++ b/data/Dockerfiles/clamav/Dockerfile @@ -1,8 +1,8 @@ -FROM debian:latest +FROM debian:stretch-slim MAINTAINER https://m-ko.de Markus Kosmal # Debian Base to use -ENV DEBIAN_VERSION jessie +ENV DEBIAN_VERSION stretch # initial install of av daemon RUN echo "deb http://http.debian.net/debian/ $DEBIAN_VERSION main contrib non-free" > /etc/apt/sources.list && \ @@ -13,15 +13,14 @@ RUN echo "deb http://http.debian.net/debian/ $DEBIAN_VERSION main contrib non-fr clamav-daemon \ clamav-freshclam \ libclamunrar7 \ - wget && \ + curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # initial update of av databases -RUN wget -O /var/lib/clamav/main.cvd http://db.local.clamav.net/main.cvd && \ - wget -O /var/lib/clamav/daily.cvd http://db.local.clamav.net/daily.cvd && \ - wget -O /var/lib/clamav/bytecode.cvd http://db.local.clamav.net/bytecode.cvd && \ - chown clamav:clamav /var/lib/clamav/*.cvd +COPY dl_files.sh /dl_files.sh +RUN chmod +x /dl_files.sh +RUN /dl_files.sh # permission juggling RUN mkdir /var/run/clamav && \ @@ -33,9 +32,6 @@ RUN sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/clamd.conf && \ echo "TCPSocket 3310" >> /etc/clamav/clamd.conf && \ sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/freshclam.conf -# volume provision -VOLUME ["/var/lib/clamav"] - # port provision EXPOSE 3310 diff --git a/data/Dockerfiles/clamav/bootstrap.sh b/data/Dockerfiles/clamav/bootstrap.sh index 635e93ea..bc5d1b32 100755 --- a/data/Dockerfiles/clamav/bootstrap.sh +++ b/data/Dockerfiles/clamav/bootstrap.sh @@ -1,35 +1,7 @@ #!/bin/bash -# bootstrap clam av service and clam av database updater shell script -# presented by mko (Markus Kosmal) -set -m +trap "kill 0" SIGINT -# start clam service itself and the updater in background as daemon freshclam -d & clamd & -# recognize PIDs -pidlist=`jobs -p` - -# initialize latest result var -latest_exit=0 - -# define shutdown helper -function shutdown() { - trap "" SUBS - - for single in $pidlist; do - if ! kill -0 $pidlist 2>/dev/null; then - wait $pidlist - exitcode=$? - fi - done - - kill $pidlist 2>/dev/null -} - -# run shutdown -trap terminate SUBS -wait - -# return received result -exit $latest_exit +sleep inf diff --git a/data/Dockerfiles/clamav/dl_files.sh b/data/Dockerfiles/clamav/dl_files.sh new file mode 100755 index 00000000..09d61241 --- /dev/null +++ b/data/Dockerfiles/clamav/dl_files.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +declare -a DB_MIRRORS=( + "switch.clamav.net" + "clamavdb.heanet.ie" + "clamav.iol.cz" + "clamav.univ-nantes.fr" + "clamav.easynet.fr" + "clamav.begi.net" +) +declare -a DB_MIRRORS=( $(shuf -e "${DB_MIRRORS[@]}") ) + +DB_FILES=( + "bytecode.cvd" + "daily.cvd" + "main.cvd" +) + +for i in "${DB_MIRRORS[@]}"; do + for j in "${DB_FILES[@]}"; do + [[ -f "/var/lib/clamav/${j}" && -s "/var/lib/clamav/${j}" ]] && continue; + if [[ $(curl -o /dev/null --connect-timeout 1 \ + --max-time 1 \ + --silent \ + --head \ + --write-out "%{http_code}\n" "${i}/${j}") == 200 ]]; then + curl "${i}/${j}" -o "/var/lib/clamav/${j}" -# + fi + done +done + +chown clamav:clamav /var/lib/clamav/*.cvd diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index abd07c1f..1d3bfbef 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,33 +1,30 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim +#ubuntu:xenial MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C +ENV DOVECOT_VERSION 2.2.29.1 +ENV PIGEONHOLE_VERSION 0.4.18 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - -RUN apt-get update -RUN apt-get -y install dovecot-common \ - dovecot-core \ - dovecot-imapd \ - dovecot-lmtpd \ - dovecot-managesieved \ - dovecot-sieve \ - dovecot-mysql \ - dovecot-pop3d \ - dovecot-dev \ +RUN apt-get update \ + && apt-get -y install libpam-dev \ + default-libmysqlclient-dev \ + lzma-dev \ + liblz-dev \ + libbz2-dev \ + liblz4-dev \ + liblzma-dev \ + build-essential \ + autotools-dev \ + automake \ syslog-ng \ syslog-ng-core \ ca-certificates \ supervisor \ wget \ curl \ - build-essential \ - autotools-dev \ - automake \ + libssl-dev \ libauthen-ntlm-perl \ libcrypt-ssleay-perl \ libdigest-hmac-perl \ @@ -52,36 +49,57 @@ RUN apt-get -y install dovecot-common \ make \ cpanminus + +RUN wget https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-$DOVECOT_VERSION \ + && ./configure --with-mysql --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib \ + && make -j3 \ + && make install \ + && make clean + +RUN wget https://pigeonhole.dovecot.org/releases/2.2/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + && ./configure \ + && make -j3 \ + && make install \ + && make clean + RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf 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 -WORKDIR /tmp - -RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | tar xvz \ - && cd /tmp/dovecot-antispam* \ - && ./autogen.sh \ - && ./configure --prefix=/usr \ - && make \ - && make install - COPY ./imapsync /usr/local/bin/imapsync COPY ./postlogin.sh /usr/local/bin/postlogin.sh COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl -COPY ./rspamd-pipe /usr/local/bin/rspamd-pipe +COPY ./report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve +COPY ./report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve +COPY ./rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham +COPY ./rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam COPY ./docker-entrypoint.sh / COPY ./supervisord.conf /etc/supervisor/supervisord.conf -RUN chmod +x /usr/local/bin/rspamd-pipe -RUN chmod +x /usr/local/bin/imapsync_cron.pl +RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ + /usr/local/lib/dovecot/sieve/rspamd-pipe-spam \ + /usr/local/bin/imapsync_cron.pl \ + /usr/local/bin/postlogin.sh \ + /usr/local/bin/imapsync -RUN groupadd -g 5000 vmail -RUN useradd -g vmail -u 5000 vmail -d /var/vmail +RUN groupadd -g 5000 vmail \ + && groupadd -g 401 dovecot \ + && groupadd -g 402 dovenull \ + && useradd -g vmail -u 5000 vmail -d /var/vmail \ + && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ + && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull EXPOSE 24 10001 ENTRYPOINT ["/docker-entrypoint.sh"] CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + /dovecot-$DOVECOT_VERSION diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 8ef09dba..a7191306 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -6,13 +6,17 @@ sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl -[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ +# Create missing directories +[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ +[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve +[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo # Set Dovecot sql config parameters, escape " in db password DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') -cat < /etc/dovecot/sql/dovecot-dict-sql.conf -connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" +# Create quota dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql.conf +connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/quota/storage table = quota2 @@ -27,28 +31,45 @@ map { } EOF -cat < /etc/dovecot/sql/dovecot-mysql.conf +# Create user and pass dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = mysql -connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" +connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" default_pass_scheme = SSHA256 password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' iterate_query = SELECT username FROM mailbox WHERE active='1'; EOF -[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve -[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo -cat /etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve -sievec /var/vmail/sieve/global.sieve -chown -R vmail:vmail /var/vmail/sieve +# Create global sieve_after script +cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve +# Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi # Create random master for SOGo sieve features RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) -echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds +# 401 is user dovecot +if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then + openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem + openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +else + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +fi + +# Compile sieve scripts +sievec /var/vmail/sieve/global.sieve +sievec /usr/local/lib/dovecot/sieve/report-spam.sieve +sievec /usr/local/lib/dovecot/sieve/report-ham.sieve + +# Fix permissions +chown -R vmail:vmail /var/vmail/sieve + + exec "$@" diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl index 5c47eb47..a3cbf8a1 100755 --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -21,7 +21,7 @@ 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 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); +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)"); $sth->execute(); my $row; @@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) { $delete2duplicates = @$row[9]; $maxage = @$row[10]; $subfolder2 = @$row[11]; + $delete1 = @$row[12]; if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } @@ -46,11 +47,12 @@ while ($row = $sth->fetchrow_arrayref()) { "--timeout1", "10", "--tmpdir", "/tmp", "--subscribeall", - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - (!defined($enc1) ? () : ($enc1)), + ($delete1 ne "1" ? () : ('--delete')), + (!defined($enc1) ? () : ($enc1)), "--host1", $host1, "--user1", $user1, "--password1", $password1, diff --git a/data/Dockerfiles/dovecot/report-ham.sieve b/data/Dockerfiles/dovecot/report-ham.sieve new file mode 100644 index 00000000..80c7f44e --- /dev/null +++ b/data/Dockerfiles/dovecot/report-ham.sieve @@ -0,0 +1,11 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +pipe :copy "rspamd-pipe-ham"; diff --git a/data/Dockerfiles/dovecot/report-spam.sieve b/data/Dockerfiles/dovecot/report-spam.sieve new file mode 100644 index 00000000..d44cb9a7 --- /dev/null +++ b/data/Dockerfiles/dovecot/report-spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy"]; + +pipe :copy "rspamd-pipe-spam"; diff --git a/data/Dockerfiles/dovecot/rspamd-pipe b/data/Dockerfiles/dovecot/rspamd-pipe deleted file mode 100755 index f9236e17..00000000 --- a/data/Dockerfiles/dovecot/rspamd-pipe +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -if [[ ${2} == "learn_spam" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnspam < /dev/stdin -elif [[ ${2} == "learn_ham" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnham < /dev/stdin -fi -# Always return 0 to satisfy Dovecot... -exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham new file mode 100755 index 00000000..7c3ab03f --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnham < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam new file mode 100755 index 00000000..67cccb2c --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnspam < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 45f9ddd5..e5a66f22 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -8,7 +8,7 @@ autostart=true stdout_syslog=true [program:dovecot] -command=/usr/sbin/dovecot -F +command=/usr/local/sbin/dovecot -F autorestart=true [program:logfiles] diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 6a36f443..210de532 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive @@ -19,12 +19,23 @@ RUN apt-get install -y --no-install-recommends supervisor \ postfix-pcre \ syslog-ng \ syslog-ng-core \ - ca-certificates + ca-certificates \ + gnupg \ + python-gpgme \ + sudo \ + curl \ + dirmngr +RUN addgroup --system --gid 600 zeyple +RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple +RUN touch /var/log/zeyple.log && chown zeyple: /var/log/zeyple.log RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf +COPY zeyple.py /usr/local/bin/zeyple.py +COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf COPY postfix.sh /opt/postfix.sh +COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh EXPOSE 588 diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c628e771..640538b0 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -17,7 +17,7 @@ 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.address 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', 'DUNNO') AS 'tls_enforce_in'; +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 @@ -25,7 +25,7 @@ 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.address 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:', 'DUNNO') AS 'tls_enforce_out'; +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'; EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -92,11 +92,24 @@ dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() EOF +# Reset GPG key permissions +mkdir -p /var/lib/zeyple/keys +chmod 700 /var/lib/zeyple/keys +chown -R 600:600 /var/lib/zeyple/keys + +# Fix Postfix permissions +chgrp -R postdrop /var/spool/postfix/public +chgrp -R postdrop /var/spool/postfix/maildrop +postfix set-permissions + +# Check Postfix configuration postconf -c /opt/postfix/conf + if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 else postfix -c /opt/postfix/conf start + supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 4268899d..72523a61 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -12,6 +12,17 @@ command=/opt/postfix.sh autorestart=true [program:postfix-maillog] -command=/usr/bin/tail -f /var/log/mail.log -stdout_logfile=/dev/fd/1 +command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log +stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 + +[unix_http_server] +file=/var/tmp/supervisord.sock +chmod=0770 +chown=nobody:nogroup + +[supervisorctl] +serverurl=unix:///var/tmp/supervisord.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh b/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh new file mode 100755 index 00000000..4ad5ab32 --- /dev/null +++ b/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +while read QUERY; do + QUERY=($QUERY) + if [ "${QUERY[0]}" != "get" ]; then + echo "500 dunno" + continue + fi + result=$(curl -s http://nginx: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/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf new file mode 100644 index 00000000..7f039582 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -0,0 +1,9 @@ +[zeyple] +log_file = /var/log/zeyple.log + +[gpg] +home = /var/lib/zeyple/keys + +[relay] +host = localhost +port = 10026 diff --git a/data/Dockerfiles/postfix/zeyple.py b/data/Dockerfiles/postfix/zeyple.py new file mode 100755 index 00000000..bb218831 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import logging +import email +import email.mime.multipart +import email.mime.application +import email.encoders +import smtplib +import copy +from io import BytesIO + +try: + from configparser import SafeConfigParser # Python 3 +except ImportError: + from ConfigParser import SafeConfigParser # Python 2 + +import gpgme + +# Boiler plate to avoid dependency on six +# BBB: Python 2.7 support +PY3K = sys.version_info > (3, 0) + + +def message_from_binary(message): + if PY3K: + return email.message_from_bytes(message) + else: + return email.message_from_string(message) + + +def as_binary_string(email): + if PY3K: + return email.as_bytes() + else: + return email.as_string() + + +def encode_string(string): + if isinstance(string, bytes): + return string + else: + return string.encode('utf-8') + + +__title__ = 'Zeyple' +__version__ = '1.2.0' +__author__ = 'Cédric Félizard' +__license__ = 'AGPLv3+' +__copyright__ = 'Copyright 2012-2016 Cédric Félizard' + + +class Zeyple: + """Zeyple Encrypts Your Precious Log Emails""" + + def __init__(self, config_fname='zeyple.conf'): + self.config = self.load_configuration(config_fname) + + log_file = self.config.get('zeyple', 'log_file') + logging.basicConfig( + filename=log_file, level=logging.DEBUG, + format='%(asctime)s %(process)s %(levelname)s %(message)s' + ) + logging.info("Zeyple ready to encrypt outgoing emails") + + def load_configuration(self, filename): + """Reads and parses the config file""" + + config = SafeConfigParser() + config.read([ + os.path.join('/etc/', filename), + filename, + ]) + if not config.sections(): + raise IOError('Cannot open config file.') + return config + + @property + def gpg(self): + protocol = gpgme.PROTOCOL_OpenPGP + + if self.config.has_option('gpg', 'executable'): + executable = self.config.get('gpg', 'executable') + else: + executable = None # Default value + + home_dir = self.config.get('gpg', 'home') + + ctx = gpgme.Context() + ctx.set_engine_info(protocol, executable, home_dir) + ctx.armor = True + + return ctx + + def process_message(self, message_data, recipients): + """Encrypts the message with recipient keys""" + message_data = encode_string(message_data) + + in_message = message_from_binary(message_data) + logging.info( + "Processing outgoing message %s", in_message['Message-id']) + + if not recipients: + logging.warn("Cannot find any recipients, ignoring") + + sent_messages = [] + for recipient in recipients: + logging.info("Recipient: %s", recipient) + + key_id = self._user_key(recipient) + logging.info("Key ID: %s", key_id) + + if key_id: + out_message = self._encrypt_message(in_message, key_id) + + # Delete Content-Transfer-Encoding if present to default to + # "7bit" otherwise Thunderbird seems to hang in some cases. + del out_message["Content-Transfer-Encoding"] + else: + logging.warn("No keys found, message will be sent unencrypted") + out_message = copy.copy(in_message) + + self._add_zeyple_header(out_message) + self._send_message(out_message, recipient) + sent_messages.append(out_message) + + return sent_messages + + def _get_version_part(self): + ret = email.mime.application.MIMEApplication( + 'Version: 1\n', + 'pgp-encrypted', + email.encoders.encode_noop, + ) + ret.add_header( + 'Content-Description', + "PGP/MIME version identification", + ) + return ret + + def _get_encrypted_part(self, payload): + ret = email.mime.application.MIMEApplication( + payload, + 'octet-stream', + email.encoders.encode_noop, + name="encrypted.asc", + ) + ret.add_header('Content-Description', "OpenPGP encrypted message") + ret.add_header( + 'Content-Disposition', + 'inline', + filename='encrypted.asc', + ) + return ret + + def _encrypt_message(self, in_message, key_id): + if in_message.is_multipart(): + # get the body (after the first \n\n) + payload = in_message.as_string().split("\n\n", 1)[1].strip() + + # prepend the Content-Type including the boundary + content_type = "Content-Type: " + in_message["Content-Type"] + payload = content_type + "\n\n" + payload + + message = email.message.Message() + message.set_payload(payload) + + payload = message.get_payload() + + else: + payload = in_message.get_payload() + payload = encode_string(payload) + + quoted_printable = email.charset.Charset('ascii') + quoted_printable.body_encoding = email.charset.QP + + message = email.mime.nonmultipart.MIMENonMultipart( + 'text', 'plain', charset='utf-8' + ) + message.set_payload(payload, charset=quoted_printable) + + mixed = email.mime.multipart.MIMEMultipart( + 'mixed', + None, + [message], + ) + + # remove superfluous header + del mixed['MIME-Version'] + + payload = as_binary_string(mixed) + + encrypted_payload = self._encrypt_payload(payload, [key_id]) + + version = self._get_version_part() + encrypted = self._get_encrypted_part(encrypted_payload) + + out_message = copy.copy(in_message) + out_message.preamble = "This is an OpenPGP/MIME encrypted " \ + "message (RFC 4880 and 3156)" + + if 'Content-Type' not in out_message: + out_message['Content-Type'] = 'multipart/encrypted' + else: + out_message.replace_header( + 'Content-Type', + 'multipart/encrypted', + ) + + out_message.set_param('protocol', 'application/pgp-encrypted') + out_message.set_payload([version, encrypted]) + + return out_message + + def _encrypt_payload(self, payload, key_ids): + """Encrypts the payload with the given keys""" + payload = encode_string(payload) + + plaintext = BytesIO(payload) + ciphertext = BytesIO() + + self.gpg.armor = True + + recipient = [self.gpg.get_key(key_id) for key_id in key_ids] + + self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) + + return ciphertext.getvalue() + + def _user_key(self, email): + """Returns the GPG key for the given email address""" + logging.info("Trying to encrypt for %s", email) + keys = [key for key in self.gpg.keylist(email)] + + if keys: + key = keys.pop() # NOTE: looks like keys[0] is the master key + key_id = key.subkeys[0].keyid + return key_id + + return None + + def _add_zeyple_header(self, message): + if self.config.has_option('zeyple', 'add_header') and \ + self.config.getboolean('zeyple', 'add_header'): + message.add_header( + 'X-Zeyple', + "processed by {0} v{1}".format(__title__, __version__) + ) + + def _send_message(self, message, recipient): + """Sends the given message through the SMTP relay""" + logging.info("Sending message %s", message['Message-id']) + + smtp = smtplib.SMTP(self.config.get('relay', 'host'), + self.config.get('relay', 'port')) + + smtp.sendmail(message['From'], recipient, message.as_string()) + smtp.quit() + + logging.info("Message %s sent", message['Message-id']) + + +if __name__ == '__main__': + recipients = sys.argv[1:] + + # BBB: Python 2.7 support + binary_stdin = sys.stdin.buffer if PY3K else sys.stdin + message = binary_stdin.read() + + zeyple = Zeyple() + zeyple.process_message(message, recipients) diff --git a/data/Dockerfiles/rmilter/Dockerfile b/data/Dockerfiles/rmilter/Dockerfile index 364c78c1..1d5db5b0 100644 --- a/data/Dockerfiles/rmilter/Dockerfile +++ b/data/Dockerfiles/rmilter/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 70a0bbab..46a97748 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get -y install rspamd ca-certificates python-pip diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 43960438..348231de 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,17 +1,12 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C ENV GOSU_VERSION 1.9 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-transport-https \ + && apt-get install -y --no-install-recommends apt-transport-https gnupg \ ca-certificates \ wget \ syslog-ng \ @@ -29,8 +24,11 @@ RUN apt-get update \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true +RUN mkdir /usr/share/doc/sogo +RUN touch /usr/share/doc/sogo/empty.sh + RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \ - && echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \ + && echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ jessie jessie" > /etc/apt/sources.list.d/sogo.list \ && apt-get update \ && apt-get -y --force-yes install sogo sogo-activesync @@ -42,10 +40,6 @@ RUN echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s COPY ./reconf-domains.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf -#EXPOSE 20000 -#EXPOSE 9191 -#EXPOSE 9192 - CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 1d05571e..b4501e1a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,9 +10,8 @@ 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 antispam -auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -ssl_protocols = !SSLv3 !SSLv2 +mail_plugins = quota acl zlib #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 ssl_options = no_compression @@ -24,12 +23,12 @@ auth_master_user_separator = * mail_prefetch_count = 30 passdb { driver = passwd-file - args = /etc/dovecot/dovecot-master.passwd + args = /usr/local/etc/dovecot/dovecot-master.passwd master = yes pass = yes } passdb { - args = /etc/dovecot/sql/dovecot-mysql.conf + args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } namespace inbox { @@ -202,15 +201,15 @@ listen = *,[::] ssl_cert = 1) + $mask = $net[1]; + $net = inet_pton($net[0]); + $addr = inet_pton($addr); + + $length = strlen($net); // 4 for IPv4, 16 for IPv6 + if (strlen($net) != strlen($addr)) + return FALSE; + if (!isset($mask)) + $mask = $length * 8; + + $addr_bin = ''; + $net_bin = ''; + for ($i = 0; $i < $length; ++$i) + { + $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT); + $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT); + } + + return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask); +} + +$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name; +$opt = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT host FROM `forwarding_hosts`"); + $networks = $stmt->fetchAll(PDO::FETCH_COLUMN); + foreach ($networks as $network) + { + if (in_net($_GET['host'], $network)) + { + echo '200 permit'; + exit; + } + } + echo '200 dunno'; +} +catch (PDOException $e) { + echo '200 dunno'; + exit; +} +?> diff --git a/data/conf/rspamd/dynmaps/settings.php b/data/conf/rspamd/dynmaps/settings.php index 9be1f696..098ffbd9 100644 --- a/data/conf/rspamd/dynmaps/settings.php +++ b/data/conf/rspamd/dynmaps/settings.php @@ -32,6 +32,35 @@ catch (PDOException $e) { ?> settings { query("SELECT `host` FROM `forwarding_hosts`"); + $rows = $stmt->fetchAll(PDO::FETCH_COLUMN); +} +catch (PDOException $e) { + $rows = array(); +} + +if ($rows) +{ +?> + whitelist_forwarding_hosts { + priority = high; + + apply "default" { + actions { + reject = 999.9; + } + } + symbols [ + "WHITELIST_FORWARDING_HOST" + ] + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'"); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -207,8 +236,11 @@ while ($row = array_shift($rows)) { } ?> apply "default" { - MAILCOW_MOO = -999.0; + MAILCOW_WHITE = -999.0; } + symbols [ + "MAILCOW_WHITE" + ] } apply "default" { - MAILCOW_MOO = 999.0; + MAILCOW_BLACK = 999.0; } + symbols [ + "MAILCOW_BLACK" + ] } -} \ No newline at end of file +} diff --git a/data/conf/rspamd/local.d/antivirus.conf b/data/conf/rspamd/local.d/antivirus.conf new file mode 100644 index 00000000..92ba684c --- /dev/null +++ b/data/conf/rspamd/local.d/antivirus.conf @@ -0,0 +1,7 @@ +clamav { + attachments_only = false; + symbol = "CLAM_VIRUS"; + type = "clamav"; + log_clean = true; + servers = "clamd:3310"; +} diff --git a/data/conf/rspamd/local.d/dkim.conf b/data/conf/rspamd/local.d/dkim.conf deleted file mode 100644 index c199c6ae..00000000 --- a/data/conf/rspamd/local.d/dkim.conf +++ /dev/null @@ -1,34 +0,0 @@ -sign_condition =< +
+
+
+ +
+
+
diff --git a/data/web/admin.php b/data/web/admin.php index 5377616f..6da0f396 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -67,6 +67,7 @@ $tfa_data = get_tfa();
@@ -81,14 +82,14 @@ $tfa_data = get_tfa();
- +
- - - - - + + + + + @@ -183,9 +184,11 @@ $tfa_data = get_tfa();

+ +
+
-

+ +
+
+
+

+ +
+
+ + + + + + + + + source; + $host = $host->host; + ?> + + + + + + + + + + + +
+
+ +
+
+
+
+ +

+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ +
+ - - + + tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} \ No newline at end of file diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index b5c69343..2e6c1afe 100644 --- a/data/web/css/mailbox.css +++ b/data/web/css/mailbox.css @@ -1,19 +1,41 @@ -.panel-heading div { - margin-top: -18px; - font-size: 15px; +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; } -.panel-heading div span { - margin-left:5px; +.pagination a { + text-decoration: none !important; } -.panel-body { - display: none; +.panel panel-default { + overflow: visible !important; } -.clickable { - cursor: pointer; +.table-responsive { + overflow: visible !important; } -.progress { - margin-bottom: 0px; +.footer-add-item { + display:block; + padding: 10px; + background: #F5F5F5; } -.table>thead>tr>th { - vertical-align: top !important; +.mass-each-action { + padding: 0 3px 0 3px; + user-select: none; +} +.mass-actions { + user-select: none; + padding:10px; +} +.mass-select-all { + cursor:pointer; + color:#555; +} +#alias_table { + cursor:pointer; +} +#alias_table .footable-paging { + cursor: auto; +} +@media (min-width: 992px) { + .container { + width: 80%; + } } \ No newline at end of file diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css index 1b3a691c..20dfb69a 100644 --- a/data/web/css/mailcow.css +++ b/data/web/css/mailcow.css @@ -50,3 +50,9 @@ pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-s -ms-user-select: none; user-select: none; } +/* Fix modal moving content left */ +body.modal-open { + overflow: inherit; + padding-right: inherit !important; +} + diff --git a/data/web/css/tables.css b/data/web/css/tables.css deleted file mode 100644 index 651e1665..00000000 --- a/data/web/css/tables.css +++ /dev/null @@ -1,79 +0,0 @@ -ul[id*="sortable"] { word-wrap: break-word; list-style-type: none; float: left; padding: 0 15px 0 0; width: 48%; cursor:move} -ul[id$="sortable-active"] li {cursor:move; } -ul[id$="sortable-inactive"] li {cursor:move } -.list-heading { cursor:default !important} -.ui-state-disabled { cursor:no-drop; color:#ccc; } -.ui-state-highlight {background: #F5F5F5 !important; height: 41px !important; cursor:move } -table[data-sortable] { - border-collapse: collapse; - border-spacing: 0; -} -table[data-sortable] th { - vertical-align: bottom; - font-weight: bold; -} -table[data-sortable] th, table[data-sortable] td { - text-align: left; - padding: 10px; -} -table[data-sortable] th:not([data-sortable="false"]) { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-touch-callout: none; - cursor: pointer; -} -table[data-sortable] th:after { - content: ""; - visibility: hidden; - display: inline-block; - vertical-align: inherit; - height: 0; - width: 0; - border-width: 5px; - border-style: solid; - border-color: transparent; - margin-right: 1px; - margin-left: 10px; - float: right; -} -table[data-sortable] th[data-sortable="false"]:after { - display: none; -} -table[data-sortable] th[data-sorted="true"]:after { - visibility: visible; -} -table[data-sortable] th[data-sorted-direction="descending"]:after { - border-top-color: inherit; - margin-top: 8px; -} -table[data-sortable] th[data-sorted-direction="ascending"]:after { - border-bottom-color: inherit; - margin-top: 3px; -} -table[data-sortable].sortable-theme-bootstrap thead th { - border-bottom: 2px solid #e0e0e0; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"] { - color: #3a87ad; - background: #d9edf7; - border-bottom-color: #bce8f1; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="descending"]:after { - border-top-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="ascending"]:after { - border-bottom-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap.sortable-theme-bootstrap-striped tbody > tr:nth-child(odd) > td { - background-color: #f9f9f9; -} -#data td, #no-data td { - vertical-align: middle; -} -.sort-table:hover { - border-bottom-color: #00B7DC !important; -} \ No newline at end of file diff --git a/data/web/delete.php b/data/web/delete.php index 6ba93d31..bfeff0f3 100644 --- a/data/web/delete.php +++ b/data/web/delete.php @@ -105,6 +105,23 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm + +
+ +
+
+ +
+
+
+ "> +
+
+
+ +
+
+
+
+
+
+ +
+
+
diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 5523ec58..97b59c0e 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -19,6 +19,23 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
+ @@ -50,11 +67,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_auth_challenge', - 'object':'', - }, + url: "/api/v1/get/u2f-authentication/", success: function(data){ data; } @@ -80,6 +93,10 @@ $(document).ready(function() { $('#YubiOTPModal').modal('show'); $("option:selected").prop("selected", false); } + if ($(this).val() == "totp") { + $('#TOTPModal').modal('show'); + $("option:selected").prop("selected", false); + } if ($(this).val() == "u2f") { $('#U2FModal').modal('show'); $("option:selected").prop("selected", false); @@ -87,11 +104,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_reg_challenge', - 'object':'', - }, + url: "/api/v1/get/u2f-registration/", success: function(data){ data; } @@ -132,25 +145,27 @@ $(document).ready(function() { // Remember last navigation pill (function () { 'use strict'; - $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { - var id = $(this).parents('[role="tablist"]').attr('id'); - var key = 'lastTag'; - if (id) { - key += ':' + id; - } - localStorage.setItem(key, $(e.target).attr('href')); - }); - $('[role="tablist"]').each(function (idx, elem) { - var id = $(elem).attr('id'); - var key = 'lastTag'; - if (id) { - key += ':' + id; - } - var lastTab = localStorage.getItem(key); - if (lastTab) { - $('[href="' + lastTab + '"]').tab('show'); - } - }); + if ($('a[data-toggle="tab"]').length) { + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + var id = $(this).parents('[role="tablist"]').attr('id'); + var key = 'lastTag'; + if (id) { + key += ':' + id; + } + localStorage.setItem(key, $(e.target).attr('href')); + }); + $('[role="tablist"]').each(function (idx, elem) { + var id = $(elem).attr('id'); + var key = 'lastTag'; + if (id) { + key += ':' + id; + } + var lastTab = localStorage.getItem(key); + if (lastTab) { + $('[href="' + lastTab + '"]').tab('show'); + } + }); + } })(); // Disable submit after submitting form diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index c37e39ab..573a1a0b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -62,64 +62,6 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } -function init_db_schema() { - // This will be much better in future releases... - global $pdo; - try { - $stmt = $pdo->prepare("SELECT NULL FROM `admin`, `imapsync`, `tfa`"); - $stmt->execute(); - } - catch (Exception $e) { - $lines = file('/web/inc/init.sql'); - $data = ''; - foreach ($lines as $line) { - if (substr($line, 0, 2) == '--' || $line == '') { - continue; - } - $data .= $line; - if (substr(trim($line), -1, 1) == ';') { - $pdo->query($data); - $data = ''; - } - } - // Create index if not exists - $stmt = $pdo->query("SHOW INDEX FROM sogo_acl WHERE KEY_NAME = 'sogo_acl_c_folder_id_idx'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("CREATE INDEX sogo_acl_c_folder_id_idx ON sogo_acl(c_folder_id)"); - } - $stmt = $pdo->query("SHOW INDEX FROM sogo_acl WHERE KEY_NAME = 'sogo_acl_c_uid_idx'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("CREATE INDEX sogo_acl_c_uid_idx ON sogo_acl(c_uid)"); - } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => 'Database initialization completed.' - ); - } - // Add newly added columns - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'kind'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `mailbox` ADD `kind` VARCHAR(100) NOT NULL DEFAULT ''"); - } - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'multiple_bookings'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'"); - } - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `mailbox` ADD `wants_tagged_subject` tinyint(1) NOT NULL DEFAULT '0'"); - } - $stmt = $pdo->query("SHOW COLUMNS FROM `tfa` LIKE 'key_id'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `tfa` ADD `key_id` VARCHAR(255) DEFAULT 'unidentified'"); - } -} function verify_ssha256($hash, $password) { // Remove tag if any $hash = ltrim($hash, '{SSHA256}'); @@ -282,13 +224,11 @@ function edit_admin_account($postarray) { $password_hashed = hash_password($password); try { $stmt = $pdo->prepare("UPDATE `admin` SET - `modified` = :modified, `password` = :password_hashed, `username` = :username1 WHERE `username` = :username2"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':modified' => date('Y-m-d H:i:s'), ':username1' => $username, ':username2' => $username_now )); @@ -304,12 +244,10 @@ function edit_admin_account($postarray) { else { try { $stmt = $pdo->prepare("UPDATE `admin` SET - `modified` = :modified, `username` = :username1 WHERE `username` = :username2"); $stmt->execute(array( ':username1' => $username, - ':modified' => date('Y-m-d H:i:s'), ':username2' => $username_now )); } @@ -603,10 +541,9 @@ function edit_user_account($postarray) { } $password_hashed = hash_password($password_new); try { - $stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':modified' => date('Y-m-d H:i:s'), ':username' => $username )); } @@ -1075,6 +1012,7 @@ function add_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $port1 = $postarray['port1']; $host1 = $postarray['host1']; $password1 = $postarray['password1']; @@ -1147,12 +1085,13 @@ function add_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) - VALUES (:user2, :exclude, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); + $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->execute(array( ':user2' => $username, ':exclude' => $exclude, ':maxage' => $maxage, + ':delete1' => $delete1, ':subfolder2' => $subfolder2, ':host1' => $host1, ':authmech1' => 'PLAIN', @@ -1200,6 +1139,7 @@ function edit_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $id = $postarray['id']; $port1 = $postarray['port1']; $host1 = $postarray['host1']; @@ -1273,10 +1213,11 @@ function edit_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("UPDATE `imapsync` set `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, `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 `user2` = :user2 AND `id` = :id"); $stmt->execute(array( ':user2' => $username, + ':delete1' => $delete1, ':id' => $id, ':exclude' => $exclude, ':maxage' => $maxage, @@ -1624,13 +1565,11 @@ function add_domain_admin($postarray) { } } try { - $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) - VALUES (:username, :password_hashed, '0', :created, :modified, :active)"); + $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, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); } @@ -1757,6 +1696,7 @@ function get_domain_admin_details($domain_admin) { 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`, @@ -1768,11 +1708,15 @@ function get_domain_admin_details($domain_admin) { ':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['active_int'] = $row['active_int']; $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; - $domainadmindata['created'] = $row['created']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $row['created']; // GET SELECTED $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain` IN ( @@ -1793,6 +1737,9 @@ function get_domain_admin_details($domain_admin) { while($row = array_shift($rows)) { $domainadmindata['unselected_domains'][] = $row['domain']; } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } } catch(PDOException $e) { $_SESSION['return'] = array( @@ -1807,6 +1754,7 @@ function set_tfa($postarray) { global $pdo; global $yubi; global $u2f; + global $tfa; if ($_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['mailcow_cc_role'] != "admin") { @@ -1903,6 +1851,36 @@ function set_tfa($postarray) { 'msg' => "U2F: " . $e->getMessage() ); $_SESSION['regReq'] = null; + return false; + } + break; + + case "totp": + (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"]; + if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) { + try { + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); + $stmt->execute(array($username, $key_id, $_POST['totp_secret'])); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['object_modified'], $username) + ); + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'TOTP verification failed' + ); } break; @@ -2023,8 +2001,16 @@ function get_tfa($username = null) { case "totp": $data['name'] = "totp"; $data['pretty'] = "Time-based OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } return $data; - break; + break; default: $data['name'] = 'none'; $data['pretty'] = "-"; @@ -2036,6 +2022,8 @@ function verify_tfa_login($username, $token) { global $pdo; global $lang; global $yubi; + global $u2f; + global $tfa; $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); @@ -2073,7 +2061,6 @@ function verify_tfa_login($username, $token) { break; case "u2f": try { - global $u2f; $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token)); $stmt = $pdo->prepare("UPDATE `tfa` SET `counter` = ? WHERE `id` = ?"); $stmt->execute(array($reg->counter, $reg->id)); @@ -2095,7 +2082,26 @@ function verify_tfa_login($username, $token) { return false; break; case "totp": + try { + $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` + WHERE `username` = :username + AND `authmech` = 'totp' + AND `active`='1'"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) { + $_SESSION['tfa_id'] = $row['id']; + return true; + } return false; + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } break; default: return false; @@ -2134,6 +2140,14 @@ function edit_domain_admin($postarray) { } } + if (empty($postarray['domain'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { $_SESSION['return'] = array( 'type' => 'danger', @@ -2164,7 +2178,7 @@ function edit_domain_admin($postarray) { return false; } - if(isset($postarray['domain'])) { + if (isset($postarray['domain'])) { foreach ($postarray['domain'] as $domain) { try { $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) @@ -2203,12 +2217,11 @@ function edit_domain_admin($postarray) { } $password_hashed = hash_password($password); try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username2"); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `active` = :active, `password` = :password_hashed WHERE `username` = :username2"); $stmt->execute(array( ':password_hashed' => $password_hashed, ':username1' => $username, ':username2' => $username_now, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); if (isset($postarray['disable_tfa'])) { @@ -2230,11 +2243,10 @@ function edit_domain_admin($postarray) { } else { try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active WHERE `username` = :username2"); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `active` = :active WHERE `username` = :username2"); $stmt->execute(array( ':username1' => $username, ':username2' => $username_now, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); if (isset($postarray['disable_tfa'])) { @@ -2296,10 +2308,9 @@ function edit_domain_admin($postarray) { } $password_hashed = hash_password($password_new); try { - $stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':modified' => date('Y-m-d H:i:s'), ':username' => $username )); } @@ -2331,7 +2342,7 @@ function get_admin_details() { return false; } try { - $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin`WHERE `superadmin`='1' AND active='1'"); + $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin` WHERE `superadmin`='1' AND active='1'"); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); } @@ -2519,6 +2530,14 @@ function mailbox_add_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0'; isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0'; @@ -2568,8 +2587,8 @@ function mailbox_add_domain($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `created`, `modified`, `active`, `relay_all_recipients`) - VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :created, :modified, :active, :relay_all_recipients)"); + $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->execute(array( ':domain' => $domain, ':description' => $description, @@ -2579,8 +2598,6 @@ function mailbox_add_domain($postarray) { ':quota' => $quota, ':backupmx' => $backupmx, ':active' => $active, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':relay_all_recipients' => $relay_all_recipients )); $_SESSION['return'] = array( @@ -2623,6 +2640,18 @@ function mailbox_add_alias($postarray) { return false; } + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `address`= :address"); + $stmt->execute(array(':address' => $address)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address)) + ); + return false; + } + foreach ($addresses as $address) { if (empty($address)) { continue; @@ -2632,6 +2661,15 @@ function mailbox_add_alias($postarray) { $local_part = strstr($address, '@', true); $address = $local_part.'@'.$domain; + $domaindata = mailbox_get_domain_details($domain); + if (is_array($domaindata) && $domaindata['aliases_left'] == "0") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['max_alias_exceeded']) + ); + return false; + } + try { $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)"); @@ -2735,16 +2773,14 @@ function mailbox_add_alias($postarray) { $goto = implode(",", $gotos); try { - $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`) - VALUES (:address, :goto, :domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`) + VALUES (:address, :goto, :domain, :active)"); if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) { $stmt->execute(array( ':address' => '@'.$domain, ':goto' => $goto, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); } @@ -2753,8 +2789,6 @@ function mailbox_add_alias($postarray) { ':address' => $address, ':goto' => $goto, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); } @@ -2855,13 +2889,11 @@ function mailbox_add_alias_domain($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `created`, `modified`, `active`) - VALUES (:alias_domain, :target_domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`) + VALUES (:alias_domain, :target_domain, :active)"); $stmt->execute(array( ':alias_domain' => $alias_domain, ':target_domain' => $target_domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); $_SESSION['return'] = array( @@ -3064,8 +3096,8 @@ function mailbox_add_mailbox($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`) - VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `active`) + VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :active)"); $stmt->execute(array( ':username' => $username, ':password_hashed' => $password_hashed, @@ -3074,8 +3106,6 @@ function mailbox_add_mailbox($postarray) { ':quota_b' => $quota_b, ':local_part' => $local_part, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); @@ -3083,14 +3113,12 @@ function mailbox_add_mailbox($postarray) { VALUES (:username, '0', '0')"); $stmt->execute(array(':username' => $username)); - $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`) - VALUES (:username1, :username2, :domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`) + VALUES (:username1, :username2, :domain, :active)"); $stmt->execute(array( ':username1' => $username, ':username2' => $username, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); @@ -3220,15 +3248,13 @@ function mailbox_add_resource($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`, `multiple_bookings`, `kind`) - VALUES (:name, 'RESOURCE', :description, 'RESOURCE', 0, :local_part, :domain, :created, :modified, :active, :multiple_bookings, :kind)"); + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`) + VALUES (:name, 'RESOURCE', :description, 'RESOURCE', 0, :local_part, :domain, :active, :multiple_bookings, :kind)"); $stmt->execute(array( ':name' => $name, ':description' => $description, ':local_part' => $local_part, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active, ':kind' => $kind, ':multiple_bookings' => $multiple_bookings @@ -3319,12 +3345,10 @@ function mailbox_edit_alias_domain($postarray) { try { $stmt = $pdo->prepare("UPDATE `alias_domain` SET `alias_domain` = :alias_domain, - `active` = :active, - `modified` = :modified + `active` = :active WHERE `alias_domain` = :alias_domain_now"); $stmt->execute(array( ':alias_domain' => $alias_domain, - ':modified' => date('Y-m-d H:i:s'), ':alias_domain_now' => $alias_domain_now, ':active' => $active )); @@ -3343,84 +3367,95 @@ function mailbox_edit_alias_domain($postarray) { ); } function mailbox_edit_alias($postarray) { - // Array elements - // address string - // goto string (separated by " ", "," ";" "\n") - email address or domain - // active int + // We can edit multiple addresses at once, but only set one "goto" and/or "active" attribute for all + // address string or array containing strings | email | required + // goto string | separated by " ", "," ";" "\n", email or domain | optional + // active set (active) or unset (inactive) global $lang; global $pdo; - $address = $postarray['address']; - $domain = idn_to_ascii(substr(strstr($address, '@'), 1)); - $local_part = strstr($address, '@', true); - if (!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($postarray['goto'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['goto_empty']) - ); - return false; - } - $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto'])); - foreach ($gotos as &$goto) { - if (empty($goto)) { - continue; - } - if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' =>sprintf($lang['danger']['goto_invalid']) - ); - return false; - } - if ($goto == $address) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['alias_goto_identical']) - ); - return false; - } - } - $gotos = array_filter($gotos); - $goto = implode(",", $gotos); + if (!is_array($postarray['address'])) { + $address_array = array(); + $address_array[] = $postarray['address']; + } + else { + $address_array = $postarray['address']; + } + if (isset($postarray['goto']) || !empty($postarray['goto'])) { + $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto'])); + foreach ($gotos as &$goto) { + if (empty($goto)) { + continue; + } + if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' =>sprintf($lang['danger']['goto_invalid']) + ); + return false; + } + if ($goto == $address) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['alias_goto_identical']) + ); + return false; + } + } + $gotos = array_filter($gotos); + $goto = implode(",", $gotos); + } isset($postarray['active']) ? $active = '1' : $active = '0'; - if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['alias_invalid']) - ); - return false; - } - - try { - $stmt = $pdo->prepare("UPDATE `alias` SET - `goto` = :goto, - `active`= :active, - `modified` = :modified - WHERE `address` = :address"); - $stmt->execute(array( - ':goto' => $goto, - ':active' => $active, - ':address' => $address, - ':modified' => date('Y-m-d H:i:s'), - )); - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars($address)) - ); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; + foreach ($address_array as $address) { + $domain = idn_to_ascii(substr(strstr($address, '@'), 1)); + $local_part = strstr($address, '@', true); + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['alias_invalid']) + ); + return false; + } + try { + if (isset($goto) && !empty($goto)) { + $stmt = $pdo->prepare("UPDATE `alias` SET + `goto` = :goto, + `active`= :active + WHERE `address` = :address"); + $stmt->execute(array( + ':goto' => $goto, + ':active' => $active, + ':address' => $address + )); + } + else { + $stmt = $pdo->prepare("UPDATE `alias` SET + `active`= :active + WHERE `address` = :address"); + $stmt->execute(array( + ':active' => $active, + ':address' => $address + )); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars(implode(', ', $address_array))) + ); } function mailbox_edit_domain($postarray) { // Array elements @@ -3432,7 +3467,7 @@ function mailbox_edit_domain($postarray) { // aliases float // mailboxes float // maxquota float - // quota float (Byte) + // quota float (Byte) // active int global $lang; @@ -3452,11 +3487,9 @@ function mailbox_edit_domain($postarray) { isset($postarray['active']) ? $active = '1' : $active = '0'; try { $stmt = $pdo->prepare("UPDATE `domain` SET - `modified`= :modified, `description` = :description WHERE `domain` = :domain"); $stmt->execute(array( - ':modified' => date('Y-m-d H:i:s'), ':description' => $description, ':domain' => $domain )); @@ -3519,6 +3552,14 @@ function mailbox_edit_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + if ($MailboxData['maxquota'] > $maxquota) { $_SESSION['return'] = array( 'type' => 'danger', @@ -3552,7 +3593,6 @@ function mailbox_edit_domain($postarray) { } try { $stmt = $pdo->prepare("UPDATE `domain` SET - `modified`= :modified, `relay_all_recipients` = :relay_all_recipients, `backupmx` = :backupmx, `active` = :active, @@ -3570,7 +3610,6 @@ function mailbox_edit_domain($postarray) { ':maxquota' => $maxquota, ':mailboxes' => $mailboxes, ':aliases' => $aliases, - ':modified' => date('Y-m-d H:i:s'), ':description' => $description, ':domain' => $domain )); @@ -3782,23 +3821,19 @@ function mailbox_edit_mailbox($postarray) { $password_hashed = hash_password($password); try { $stmt = $pdo->prepare("UPDATE `alias` SET - `modified` = :modified, `active` = :active WHERE `address` = :address"); $stmt->execute(array( ':address' => $username, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); $stmt = $pdo->prepare("UPDATE `mailbox` SET - `modified` = :modified, `active` = :active, `password` = :password_hashed, `name`= :name, `quota` = :quota_b WHERE `username` = :username"); $stmt->execute(array( - ':modified' => date('Y-m-d H:i:s'), ':password_hashed' => $password_hashed, ':active' => $active, ':name' => $name, @@ -3821,23 +3856,19 @@ function mailbox_edit_mailbox($postarray) { } try { $stmt = $pdo->prepare("UPDATE `alias` SET - `modified` = :modified, `active` = :active WHERE `address` = :address"); $stmt->execute(array( ':address' => $username, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); $stmt = $pdo->prepare("UPDATE `mailbox` SET - `modified` = :modified, `active` = :active, `name`= :name, `quota` = :quota_b WHERE `username` = :username"); $stmt->execute(array( ':active' => $active, - ':modified' => date('Y-m-d H:i:s'), ':name' => $name, ':quota_b' => $quota_b, ':username' => $username @@ -3900,7 +3931,6 @@ function mailbox_edit_resource($postarray) { try { $stmt = $pdo->prepare("UPDATE `mailbox` SET - `modified` = :modified, `active` = :active, `name`= :description, `kind`= :kind, @@ -3908,7 +3938,6 @@ function mailbox_edit_resource($postarray) { WHERE `username` = :name"); $stmt->execute(array( ':active' => $active, - ':modified' => date('Y-m-d H:i:s'), ':description' => $description, ':multiple_bookings' => $multiple_bookings, ':kind' => $kind, @@ -4137,6 +4166,17 @@ function mailbox_get_alias_details($address) { ':address' => $address, )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain"); + $stmt->execute(array( + ':domain' => $row['domain'], + )); + $row_alias_domain = $stmt->fetch(PDO::FETCH_ASSOC); + if (isset($row_alias_domain['target_domain']) && !empty($row_alias_domain['target_domain'])) { + $aliasdata['in_primary_domain'] = $row_alias_domain['target_domain']; + } + else { + $aliasdata['in_primary_domain'] = ""; + } $aliasdata['domain'] = $row['domain']; $aliasdata['goto'] = $row['goto']; $aliasdata['address'] = $row['address']; @@ -4253,6 +4293,15 @@ function mailbox_get_domain_details($domain) { } try { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain"); + $stmt->execute(array( + ':domain' => $domain + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!empty($row)) { + $domain = $row['target_domain']; + } + $stmt = $pdo->prepare("SELECT `domain`, `description`, @@ -4268,10 +4317,18 @@ function mailbox_get_domain_details($domain) { CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` FROM `domain` WHERE `domain`= :domain"); $stmt->execute(array( - ':domain' => $domain, + ':domain' => $domain )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain"); + if (empty($row)) { + return false; + } + + $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, + COALESCE(SUM(`quota`), 0) AS `in_use` + FROM `mailbox` + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `domain` = :domain"); $stmt->execute(array(':domain' => $row['domain'])); $MailboxDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); @@ -4296,15 +4353,17 @@ function mailbox_get_domain_details($domain) { $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients_int']; $stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias` - WHERE `domain`= :domain + WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2)) AND `address` NOT IN ( SELECT `username` FROM `mailbox` )"); $stmt->execute(array( ':domain' => $domain, + ':domain2' => $domain )); - $AliasData = $stmt->fetch(PDO::FETCH_ASSOC); - (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $AliasDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); + (isset($AliasDataDomain['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasDataDomain['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $domaindata['aliases_left'] = $row['aliases'] - $AliasDataDomain['alias_count']; } catch (PDOException $e) { $_SESSION['return'] = array( @@ -4540,48 +4599,57 @@ function mailbox_delete_domain($postarray) { return true; } function mailbox_delete_alias($postarray) { + // $postarray['address'] can be a single element or an array global $lang; global $pdo; - $address = $postarray['address']; - $local_part = strstr($address, '@', true); - $domain = mailbox_get_alias_details($address)['domain']; - try { - $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address"); - $stmt->execute(array(':address' => $address)); - $gotos = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - $goto_array = explode(',', $gotos['goto']); + if (!is_array($postarray['address'])) { + $address_array = array(); + $address_array[] = $postarray['address']; + } + else { + $address_array = $postarray['address']; + } + foreach ($address_array as $address) { + $local_part = strstr($address, '@', true); + $domain = mailbox_get_alias_details($address)['domain']; + try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address"); + $stmt->execute(array(':address' => $address)); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $goto_array = explode(',', $gotos['goto']); - if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - try { - $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)"); - $stmt->execute(array( - ':address' => $address - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)"); + $stmt->execute(array( + ':address' => $address + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } $_SESSION['return'] = array( 'type' => 'success', - 'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars($address)) + 'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars(implode(', ', $address_array))) ); } @@ -4729,12 +4797,10 @@ function mailbox_delete_mailbox($postarray) { } $gotos_rebuild = implode(',', $goto_exploded); $stmt = $pdo->prepare("UPDATE `alias` SET - `goto` = :goto, - `modified` = :modified, + `goto` = :goto WHERE `address` = :address"); $stmt->execute(array( ':goto' => $gotos_rebuild, - ':modified' => date('Y-m-d H:i:s'), ':address' => $gotos['address'] )); } @@ -4972,4 +5038,93 @@ function get_u2f_registrations($username) { $sel->execute(array($username)); return $sel->fetchAll(PDO::FETCH_OBJ); } +function get_forwarding_hosts() { + global $pdo; + $sel = $pdo->prepare("SELECT host, source FROM `forwarding_hosts`"); + $sel->execute(); + return $sel->fetchAll(PDO::FETCH_OBJ); +} +function add_forwarding_host($postarray) { + require_once 'spf.inc.php'; + global $pdo; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $source = $postarray['hostname']; + $host = $postarray['hostname']; + $hosts = array(); + if (preg_match('/^[0-9a-fA-F:\/]+$/', $host)) { // IPv6 address + $hosts = array($host); + } + elseif (preg_match('/^[0-9\.\/]+$/', $host)) { // IPv4 address + $hosts = array($host); + } + else { + $hosts = get_outgoing_hosts_best_guess($host); + } + if (!$hosts) + { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid host specified: '. htmlspecialchars($host) + ); + return false; + } + foreach ($hosts as $host) { + if ($source == $host) + $source = ''; + try { + $stmt = $pdo->prepare("INSERT IGNORE INTO `forwarding_hosts` (`host`, `source`) VALUES (:host, :source)"); + $stmt->execute(array( + ':host' => $host, + ':source' => $source, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['forwarding_host_added'], htmlspecialchars(implode(', ', $hosts))) + ); +} +function delete_forwarding_host($postarray) { + global $pdo; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $host = $postarray['forwardinghost']; + try { + $stmt = $pdo->prepare("DELETE FROM `forwarding_hosts` WHERE `host` = :host"); + $stmt->execute(array( + ':host' => $host, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['forwarding_host_removed'], htmlspecialchars($host)) + ); +} ?> diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a206a35d..678c2590 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -16,11 +16,10 @@ - - ' : null;?> +' : null;?> @@ -57,7 +56,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { ?>