浏览代码

Merge pull request #185 from andryyy/dev

Merge from master to dev
André Peters 8 年之前
父节点
当前提交
9633a34f9f
共有 44 个文件被更改,包括 1022 次插入682 次删除
  1. 52 34
      data/Dockerfiles/dovecot/Dockerfile
  2. 30 9
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  3. 7 5
      data/Dockerfiles/dovecot/imapsync_cron.pl
  4. 11 0
      data/Dockerfiles/dovecot/report-ham.sieve
  5. 3 0
      data/Dockerfiles/dovecot/report-spam.sieve
  6. 0 8
      data/Dockerfiles/dovecot/rspamd-pipe
  7. 4 0
      data/Dockerfiles/dovecot/rspamd-pipe-ham
  8. 4 0
      data/Dockerfiles/dovecot/rspamd-pipe-spam
  9. 1 1
      data/Dockerfiles/dovecot/supervisord.conf
  10. 12 3
      data/Dockerfiles/postfix/Dockerfile
  11. 15 2
      data/Dockerfiles/postfix/postfix.sh
  12. 13 2
      data/Dockerfiles/postfix/supervisord.conf
  13. 9 0
      data/Dockerfiles/postfix/zeyple.conf
  14. 274 0
      data/Dockerfiles/postfix/zeyple.py
  15. 2 7
      data/Dockerfiles/rmilter/Dockerfile
  16. 2 7
      data/Dockerfiles/rspamd/Dockerfile
  17. 6 12
      data/Dockerfiles/sogo/Dockerfile
  18. 26 18
      data/conf/dovecot/dovecot.conf
  19. 10 0
      data/conf/nginx/site.conf
  20. 1 0
      data/conf/postfix/main.cf
  21. 12 0
      data/conf/postfix/master.cf
  22. 7 0
      data/web/add.php
  23. 14 8
      data/web/admin.php
  24. 13 0
      data/web/css/admin.css
  25. 14 14
      data/web/css/mailbox.css
  26. 0 79
      data/web/css/tables.css
  27. 14 0
      data/web/edit.php
  28. 2 10
      data/web/inc/footer.inc.php
  29. 76 9
      data/web/inc/functions.inc.php
  30. 1 2
      data/web/inc/header.inc.php
  31. 1 1
      data/web/js/add.js
  32. 40 29
      data/web/js/admin.js
  33. 11 11
      data/web/js/mailbox.js
  34. 0 236
      data/web/js/sorttable.js
  35. 218 144
      data/web/json_api.php
  36. 9 1
      data/web/lang/lang.de.php
  37. 9 1
      data/web/lang/lang.en.php
  38. 1 0
      data/web/lang/lang.ru.php
  39. 8 26
      data/web/mailbox.php
  40. 2 2
      data/web/user.php
  41. 4 1
      docker-compose.yml
  42. 33 0
      docs/first_steps.md
  43. 4 0
      docs/install.md
  44. 47 0
      docs/u_and_e.md

+ 52 - 34
data/Dockerfiles/dovecot/Dockerfile

@@ -1,33 +1,30 @@
-FROM ubuntu:xenial
+FROM debian:stretch-slim
+#ubuntu:xenial
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 ENV LC_ALL C
+ENV DOVECOT_VERSION 2.2.28
+ENV PIGEONHOLE_VERSION 0.4.17
 
 
-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 \
 	syslog-ng-core \
 	syslog-ng-core \
 	ca-certificates \
 	ca-certificates \
 	supervisor \
 	supervisor \
 	wget \
 	wget \
 	curl \
 	curl \
-	build-essential \
-	autotools-dev \
-	automake \
+	libssl-dev \
 	libauthen-ntlm-perl \
 	libauthen-ntlm-perl \
 	libcrypt-ssleay-perl \
 	libcrypt-ssleay-perl \
 	libdigest-hmac-perl \
 	libdigest-hmac-perl \
@@ -52,36 +49,57 @@ RUN apt-get -y install dovecot-common \
 	make \
 	make \
 	cpanminus
 	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 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 cpanm Data::Uniqid Mail::IMAPClient String::Util
 RUN echo '* * * * *   root   /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync
 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/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 ./imapsync /usr/local/bin/imapsync
 COPY ./postlogin.sh /usr/local/bin/postlogin.sh
 COPY ./postlogin.sh /usr/local/bin/postlogin.sh
 COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl
 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 ./docker-entrypoint.sh /
 COPY ./supervisord.conf /etc/supervisor/supervisord.conf
 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
 EXPOSE 24 10001
 
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
 ENTRYPOINT ["/docker-entrypoint.sh"]
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 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

+ 30 - 9
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -6,12 +6,16 @@ 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 "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl
 sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /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
 # Set Dovecot sql config parameters, escape " in db password
 DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 
 
-cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql.conf
+# Create quota dict for Dovecot
+cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql.conf
 connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}"
 connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}"
 map {
 map {
   pattern = priv/quota/storage
   pattern = priv/quota/storage
@@ -27,7 +31,8 @@ map {
 }
 }
 EOF
 EOF
 
 
-cat <<EOF > /etc/dovecot/sql/dovecot-mysql.conf
+# Create user and pass dict for Dovecot
+cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-mysql.conf
 driver = mysql
 driver = mysql
 connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}"
 connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}"
 default_pass_scheme = SSHA256
 default_pass_scheme = SSHA256
@@ -36,19 +41,35 @@ user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid,
 iterate_query = SELECT username FROM mailbox WHERE active='1';
 iterate_query = SELECT username FROM mailbox WHERE active='1';
 EOF
 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.
 # 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
 if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi
 
 
 # Create random master for SOGo sieve features
 # Create random master for SOGo sieve features
 RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
 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)
 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
 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 "$@"
 exec "$@"

+ 7 - 5
data/Dockerfiles/dovecot/imapsync_cron.pl

@@ -21,7 +21,7 @@ open my $file, '<', "/etc/sogo/sieve.creds";
 my $creds = <$file>; 
 my $creds = <$file>; 
 close $file;
 close $file;
 my ($master_user, $master_pass) = split /:/, $creds;
 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();
 $sth->execute();
 my $row;
 my $row;
 
 
@@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) {
   $delete2duplicates  = @$row[9];
   $delete2duplicates  = @$row[9];
   $maxage             = @$row[10];
   $maxage             = @$row[10];
   $subfolder2         = @$row[11];
   $subfolder2         = @$row[11];
+  $delete1            = @$row[12];
 
 
   if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
   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",
 	"--timeout1", "10",
 	"--tmpdir", "/tmp",
 	"--tmpdir", "/tmp",
 	"--subscribeall",
 	"--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')),
 	($delete2duplicates	ne "1"	? () : ('--delete2duplicates')),
-	(!defined($enc1)			? () : ($enc1)),
+	($delete1	ne "1"	? () : ('--delete')),
+	(!defined($enc1) ? () : ($enc1)),
 	"--host1", $host1,
 	"--host1", $host1,
 	"--user1", $user1,
 	"--user1", $user1,
 	"--password1", $password1,
 	"--password1", $password1,

+ 11 - 0
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";

+ 3 - 0
data/Dockerfiles/dovecot/report-spam.sieve

@@ -0,0 +1,3 @@
+require ["vnd.dovecot.pipe", "copy"];
+
+pipe :copy "rspamd-pipe-spam";

+ 0 - 8
data/Dockerfiles/dovecot/rspamd-pipe

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

+ 4 - 0
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

+ 4 - 0
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

+ 1 - 1
data/Dockerfiles/dovecot/supervisord.conf

@@ -8,7 +8,7 @@ autostart=true
 stdout_syslog=true
 stdout_syslog=true
 
 
 [program:dovecot]
 [program:dovecot]
-command=/usr/sbin/dovecot -F
+command=/usr/local/sbin/dovecot -F
 autorestart=true
 autorestart=true
 
 
 [program:logfiles]
 [program:logfiles]

+ 12 - 3
data/Dockerfiles/postfix/Dockerfile

@@ -1,4 +1,4 @@
-FROM ubuntu:xenial
+FROM debian:testing-slim
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV DEBIAN_FRONTEND noninteractive
@@ -19,10 +19,19 @@ RUN apt-get install -y --no-install-recommends supervisor \
 	postfix-pcre \
 	postfix-pcre \
 	syslog-ng \
 	syslog-ng \
 	syslog-ng-core \
 	syslog-ng-core \
-	ca-certificates
-
+	ca-certificates \
+	gnupg \
+	python-gpgme \
+	sudo \
+	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
 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 supervisord.conf /etc/supervisor/supervisord.conf
 COPY postfix.sh /opt/postfix.sh
 COPY postfix.sh /opt/postfix.sh
 
 

+ 15 - 2
data/Dockerfiles/postfix/postfix.sh

@@ -17,7 +17,7 @@ user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = mysql
 hosts = mysql
 dbname = ${DBNAME}
 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
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
@@ -25,7 +25,7 @@ user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = mysql
 hosts = mysql
 dbname = ${DBNAME}
 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
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
 cat <<EOF > /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()
 query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP()
 EOF
 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
 postconf -c /opt/postfix/conf
+
 if [[ $? != 0 ]]; then
 if [[ $? != 0 ]]; then
 	echo "Postfix configuration error, refusing to start."
 	echo "Postfix configuration error, refusing to start."
 	exit 1
 	exit 1
 else
 else
 	postfix -c /opt/postfix/conf start
 	postfix -c /opt/postfix/conf start
+	supervisorctl restart postfix-maillog
 	sleep 126144000
 	sleep 126144000
 fi
 fi

+ 13 - 2
data/Dockerfiles/postfix/supervisord.conf

@@ -12,6 +12,17 @@ command=/opt/postfix.sh
 autorestart=true
 autorestart=true
 
 
 [program:postfix-maillog]
 [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
 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

+ 9 - 0
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

+ 274 - 0
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)

+ 2 - 7
data/Dockerfiles/rmilter/Dockerfile

@@ -1,16 +1,11 @@
-FROM ubuntu:xenial
+FROM debian:jessie-slim
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 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 \
 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 update \
 	&& apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor
 	&& apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor
 
 

+ 2 - 7
data/Dockerfiles/rspamd/Dockerfile

@@ -1,16 +1,11 @@
-FROM ubuntu:xenial
+FROM debian:jessie-slim
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 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 \
 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 update \
     && apt-get -y install rspamd ca-certificates python-pip
     && apt-get -y install rspamd ca-certificates python-pip
 
 

+ 6 - 12
data/Dockerfiles/sogo/Dockerfile

@@ -1,17 +1,12 @@
-FROM ubuntu:xenial
+FROM debian:jessie-slim
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 ENV LC_ALL C
 ENV GOSU_VERSION 1.9
 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 \
 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 \
 		ca-certificates \
 		wget \
 		wget \
 		syslog-ng \
 		syslog-ng \
@@ -29,8 +24,11 @@ RUN apt-get update \
     && chmod +x /usr/local/bin/gosu \
     && chmod +x /usr/local/bin/gosu \
     && gosu nobody true
     && 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 \
 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 update \
 	&& apt-get -y --force-yes install sogo sogo-activesync 
 	&& 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 ./reconf-domains.sh /
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 
 
-#EXPOSE 20000
-#EXPOSE 9191
-#EXPOSE 9192
-
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 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/*

+ 26 - 18
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"
 login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
 mail_home = /var/vmail/%d/%n
 mail_home = /var/vmail/%d/%n
 mail_location = maildir:~/
 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_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_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
 ssl_options = no_compression
@@ -24,12 +23,12 @@ auth_master_user_separator = *
 mail_prefetch_count = 30
 mail_prefetch_count = 30
 passdb {
 passdb {
   driver = passwd-file
   driver = passwd-file
-  args = /etc/dovecot/dovecot-master.passwd
+  args = /usr/local/etc/dovecot/dovecot-master.passwd
   master = yes
   master = yes
   pass = yes
   pass = yes
 }
 }
 passdb {
 passdb {
-  args = /etc/dovecot/sql/dovecot-mysql.conf
+  args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf
   driver = sql
   driver = sql
 }
 }
 namespace inbox {
 namespace inbox {
@@ -202,15 +201,15 @@ listen = *,[::]
 ssl_cert = </etc/ssl/mail/cert.pem
 ssl_cert = </etc/ssl/mail/cert.pem
 ssl_key = </etc/ssl/mail/key.pem
 ssl_key = </etc/ssl/mail/key.pem
 userdb {
 userdb {
-  args = /etc/dovecot/sql/dovecot-mysql.conf
+  args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf
   driver = sql
   driver = sql
 }
 }
 protocol imap {
 protocol imap {
-  mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib antispam
+  mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve #mail_crypt
 }
 }
 protocol lmtp {
 protocol lmtp {
-  mail_plugins = quota sieve acl zlib
-  auth_socket_path = /var/run/dovecot/auth-master
+  mail_plugins = quota sieve acl zlib #mail_crypt
+  auth_socket_path = /usr/local/var/run/dovecot/auth-master
 }
 }
 protocol sieve {
 protocol sieve {
   managesieve_logout_format = bytes=%i/%o
   managesieve_logout_format = bytes=%i/%o
@@ -221,22 +220,31 @@ plugin {
   acl = vfile
   acl = vfile
   quota = dict:Userquota::proxy::sqlquota
   quota = dict:Userquota::proxy::sqlquota
   quota_rule2 = Trash:storage=+100%%
   quota_rule2 = Trash:storage=+100%%
-  antispam_backend = mailtrain
-  antispam_spam    = Junk
-  antispam_trash   = Trash
-  antispam_mail_sendmail = /usr/local/bin/rspamd-pipe
-  antispam_mail_spam     = learn_spam
-  antispam_mail_notspam  = learn_ham
-  # Do not complain about empty parameter
-  antispam_mail_sendmail_args = --blind
   sieve = /var/vmail/sieve/%u.sieve
   sieve = /var/vmail/sieve/%u.sieve
+  sieve_plugins = sieve_imapsieve sieve_extprograms
+  # From elsewhere to Spam folder
+  imapsieve_mailbox1_name = Junk
+  imapsieve_mailbox1_causes = COPY
+  imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve
+  # END
+  # From Spam folder to elsewhere
+  imapsieve_mailbox2_name = *
+  imapsieve_mailbox2_from = Junk
+  imapsieve_mailbox2_causes = COPY
+  imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve
+  # END
+  sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
+  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
   sieve_after = /var/vmail/sieve/global.sieve
   sieve_after = /var/vmail/sieve/global.sieve
   sieve_max_script_size = 1M
   sieve_max_script_size = 1M
   sieve_quota_max_scripts = 0
   sieve_quota_max_scripts = 0
   sieve_quota_max_storage = 0
   sieve_quota_max_storage = 0
+  #mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
+  #mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
+  #mail_crypt_save_version = 2
 }
 }
 dict {
 dict {
-  sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql.conf
+  sqlquota = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql.conf
 }
 }
 remote 127.0.0.1 {
 remote 127.0.0.1 {
   disable_plaintext_auth = no
   disable_plaintext_auth = no

+ 10 - 0
data/conf/nginx/site.conf

@@ -18,6 +18,11 @@ server {
   access_log /var/log/nginx/access.log;
   access_log /var/log/nginx/access.log;
   root /web;
   root /web;
 
 
+  location /api/v1/ {
+    try_files $uri $uri/ /json_api.php?$args;
+  }
+  rewrite ^/api/v1/([^/]+)/([^/]+)/?$ /json_api.php?action=$1&object=$2? last;
+
   location ^~ /.well-known/acme-challenge/ {
   location ^~ /.well-known/acme-challenge/ {
 	  allow all;
 	  allow all;
     default_type "text/plain";
     default_type "text/plain";
@@ -166,6 +171,11 @@ server {
   access_log /var/log/nginx/access.log;
   access_log /var/log/nginx/access.log;
   root /web;
   root /web;
 
 
+  location /api/v1/ {
+    try_files $uri $uri/ /json_api.php?$args;
+  }
+  rewrite ^/api/v1/([^/]+)/([^/]+)/?$ /json_api.php?action=$1&object=$2? last;
+
   location ^~ /.well-known/acme-challenge/ {
   location ^~ /.well-known/acme-challenge/ {
 	  allow all;
 	  allow all;
     default_type "text/plain";
     default_type "text/plain";

+ 1 - 0
data/conf/postfix/main.cf

@@ -91,3 +91,4 @@ smtpd_milters = inet:rmilter:9900
 non_smtpd_milters = inet:rmilter:9900
 non_smtpd_milters = inet:rmilter:9900
 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
 mydestination = localhost.localdomain, localhost
 mydestination = localhost.localdomain, localhost
+#content_filter=zeyple

+ 12 - 0
data/conf/postfix/master.cf

@@ -16,6 +16,7 @@ smtp_enforced_tls      unix  -       -       n       -       -       smtp
   -o smtp_tls_security_level=encrypt
   -o smtp_tls_security_level=encrypt
   -o syslog_name=enforced-tls-smtp
   -o syslog_name=enforced-tls-smtp
   -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
   -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
+
 tlsproxy   unix  -       -       n       -       0       tlsproxy
 tlsproxy   unix  -       -       n       -       0       tlsproxy
 dnsblog    unix  -       -       n       -       0       dnsblog
 dnsblog    unix  -       -       n       -       0       dnsblog
 pickup     fifo  n       -       n       60      1       pickup
 pickup     fifo  n       -       n       60      1       pickup
@@ -43,3 +44,14 @@ anvil      unix  -       -       n       -       1       anvil
 scache     unix  -       -       n       -       1       scache
 scache     unix  -       -       n       -       1       scache
 maildrop   unix  -       n       n       -       -       pipe flags=DRhu
 maildrop   unix  -       n       n       -       -       pipe flags=DRhu
     user=vmail argv=/usr/bin/maildrop -d ${recipient}
     user=vmail argv=/usr/bin/maildrop -d ${recipient}
+zeyple    unix  -       n       n       -       -       pipe
+  user=zeyple argv=/usr/local/bin/zeyple.py ${recipient}
+127.0.0.1:10026 inet  n       -       n       -       10      smtpd
+  -o content_filter=
+  -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
+  -o smtpd_helo_restrictions=
+  -o smtpd_client_restrictions=
+  -o smtpd_sender_restrictions=
+  -o smtpd_recipient_restrictions=permit_mynetworks,reject
+  -o mynetworks=127.0.0.0/8
+  -o smtpd_authorized_xforward_hosts=127.0.0.0/8

+ 7 - 0
data/web/add.php

@@ -350,6 +350,13 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
 							</div>
 							</div>
 						</div>
 						</div>
 					</div>
 					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="delete1" checked> <?=$lang['add']['delete1'];?></label>
+							</div>
+						</div>
+					</div>
 					<div class="form-group">
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 						<div class="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">
 							<div class="checkbox">

+ 14 - 8
data/web/admin.php

@@ -81,14 +81,14 @@ $tfa_data = get_tfa();
         <div class="panel-body">
         <div class="panel-body">
           <form method="post">
           <form method="post">
             <div class="table-responsive">
             <div class="table-responsive">
-            <table class="table table-striped sortable-theme-bootstrap" data-sortable id="domainadminstable">
+            <table class="table table-striped" id="domainadminstable">
               <thead>
               <thead>
               <tr>
               <tr>
-                <th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
-                <th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
-                <th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
-                <th class="sort-table" style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th>
-                <th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th>
+                <th style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
+                <th style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
+                <th style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
+                <th style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th>
+                <th style="text-align: right; min-width: 200px;"><?=$lang['admin']['action'];?></th>
               </tr>
               </tr>
               </thead>
               </thead>
               <tbody>
               <tbody>
@@ -299,8 +299,14 @@ $tfa_data = get_tfa();
   </div>
   </div>
   </div>
   </div>
 </div> <!-- /container -->
 </div> <!-- /container -->
-<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script>
-<script src="js/sorttable.js"></script>
+<script type='text/javascript'>
+<?php
+$lang_admin = json_encode($lang['admin']);
+echo "var lang = ". $lang_admin . ";\n";
+echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
+?>
+</script>
+<script src="js/footable.min.js"></script>
 <script src="js/admin.js"></script>
 <script src="js/admin.js"></script>
 <?php
 <?php
 require_once("inc/footer.inc.php");
 require_once("inc/footer.inc.php");

+ 13 - 0
data/web/css/admin.css

@@ -0,0 +1,13 @@
+table.footable>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;
+}

+ 14 - 14
data/web/css/mailbox.css

@@ -1,19 +1,19 @@
-.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;
-}
-.table>thead>tr>th {
-  vertical-align: top !important;
+.footer-add-item {
+  text-align:center;
+  font-style: italic;
+  display:block;
+  padding: 10px;
 }
 }

+ 0 - 79
data/web/css/tables.css

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

+ 14 - 0
data/web/edit.php

@@ -620,6 +620,20 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
 						<input type="text" class="form-control" name="exclude" id="exclude" value="<?=htmlspecialchars($result['exclude'], ENT_QUOTES, 'UTF-8');?>">
 						<input type="text" class="form-control" name="exclude" id="exclude" value="<?=htmlspecialchars($result['exclude'], ENT_QUOTES, 'UTF-8');?>">
 						</div>
 						</div>
 					</div>
 					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="delete2duplicates" <?=($result['delete2duplicates']=="1") ? "checked" : "";?>> <?=$lang['edit']['delete2duplicates'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="delete1" <?=($result['delete1']=="1") ? "checked" : "";?>> <?=$lang['edit']['delete1'];?></label>
+							</div>
+						</div>
+					</div>
 					<div class="form-group">
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 						<div class="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">
 							<div class="checkbox">

+ 2 - 10
data/web/inc/footer.inc.php

@@ -50,11 +50,7 @@ $(document).ready(function() {
           type: "GET",
           type: "GET",
           cache: false,
           cache: false,
           dataType: 'script',
           dataType: 'script',
-          url: "json_api.php",
-          data: {
-            'action':'get_u2f_auth_challenge',
-            'object':'<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>',
-          },
+          url: "/api/v1/u2f-authentication/<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>",
           success: function(data){
           success: function(data){
             data;
             data;
           }
           }
@@ -87,11 +83,7 @@ $(document).ready(function() {
         type: "GET",
         type: "GET",
         cache: false,
         cache: false,
         dataType: 'script',
         dataType: 'script',
-        url: "json_api.php",
-        data: {
-          'action':'get_u2f_reg_challenge',
-          'object':'<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>',
-        },
+        url: "/api/v1/u2f-registration/<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>",
         success: function(data){
         success: function(data){
           data;
           data;
         }
         }

+ 76 - 9
data/web/inc/functions.inc.php

@@ -109,6 +109,11 @@ function init_db_schema() {
   if ($num_results == 0) {
   if ($num_results == 0) {
     $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'");
     $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'");
   }
   }
+  $stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'delete1'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results == 0) {
+    $pdo->query("ALTER TABLE `imapsync` ADD `delete1` tinyint(1) NOT NULL DEFAULT '0'");
+  }
   $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'");
   $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'");
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
   if ($num_results == 0) {
   if ($num_results == 0) {
@@ -1075,6 +1080,7 @@ function add_syncjob($postarray) {
   }
   }
   isset($postarray['active']) ? $active = '1' : $active = '0';
   isset($postarray['active']) ? $active = '1' : $active = '0';
   isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
   isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+  isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0';
   $port1            = $postarray['port1'];
   $port1            = $postarray['port1'];
   $host1            = $postarray['host1'];
   $host1            = $postarray['host1'];
   $password1        = $postarray['password1'];
   $password1        = $postarray['password1'];
@@ -1147,12 +1153,13 @@ function add_syncjob($postarray) {
     return false;
     return false;
   }
   }
   try {
   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(
     $stmt->execute(array(
       ':user2' => $username,
       ':user2' => $username,
       ':exclude' => $exclude,
       ':exclude' => $exclude,
       ':maxage' => $maxage,
       ':maxage' => $maxage,
+      ':delete1' => $delete1,
       ':subfolder2' => $subfolder2,
       ':subfolder2' => $subfolder2,
       ':host1' => $host1,
       ':host1' => $host1,
       ':authmech1' => 'PLAIN',
       ':authmech1' => 'PLAIN',
@@ -1200,6 +1207,7 @@ function edit_syncjob($postarray) {
   }
   }
   isset($postarray['active']) ? $active = '1' : $active = '0';
   isset($postarray['active']) ? $active = '1' : $active = '0';
   isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
   isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+  isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0';
   $id               = $postarray['id'];
   $id               = $postarray['id'];
   $port1            = $postarray['port1'];
   $port1            = $postarray['port1'];
   $host1            = $postarray['host1'];
   $host1            = $postarray['host1'];
@@ -1273,10 +1281,11 @@ function edit_syncjob($postarray) {
     return false;
     return false;
   }
   }
   try {
   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");
       WHERE `user2` = :user2 AND `id` = :id");
     $stmt->execute(array(
     $stmt->execute(array(
       ':user2' => $username,
       ':user2' => $username,
+      ':delete1' => $delete1,
       ':id' => $id,
       ':id' => $id,
       ':exclude' => $exclude,
       ':exclude' => $exclude,
       ':maxage' => $maxage,
       ':maxage' => $maxage,
@@ -1757,6 +1766,7 @@ function get_domain_admin_details($domain_admin) {
   try {
   try {
     $stmt = $pdo->prepare("SELECT
     $stmt = $pdo->prepare("SELECT
       `tfa`.`active` AS `tfa_active_int`,
       `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`.`username`,
       `domain_admins`.`created`,
       `domain_admins`.`created`,
       `domain_admins`.`active` AS `active_int`,
       `domain_admins`.`active` AS `active_int`,
@@ -1768,11 +1778,15 @@ function get_domain_admin_details($domain_admin) {
       ':domain_admin' => $domain_admin
       ':domain_admin' => $domain_admin
     ));
     ));
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (empty($row)) { 
+      return false;
+    }
     $domainadmindata['username'] = $row['username'];
     $domainadmindata['username'] = $row['username'];
+    $domainadmindata['tfa_active'] = $row['tfa_active'];
     $domainadmindata['active'] = $row['active'];
     $domainadmindata['active'] = $row['active'];
-    $domainadmindata['active_int'] = $row['active_int'];
     $domainadmindata['tfa_active_int'] = $row['tfa_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
     // GET SELECTED
     $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
     $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
       WHERE `domain` IN (
       WHERE `domain` IN (
@@ -1793,6 +1807,9 @@ function get_domain_admin_details($domain_admin) {
     while($row = array_shift($rows)) {
     while($row = array_shift($rows)) {
       $domainadmindata['unselected_domains'][] = $row['domain'];
       $domainadmindata['unselected_domains'][] = $row['domain'];
     }
     }
+    if (!isset($domainadmindata['unselected_domains'])) {
+      $domainadmindata['unselected_domains'] = "";
+    }
   }
   }
   catch(PDOException $e) {
   catch(PDOException $e) {
     $_SESSION['return'] = array(
     $_SESSION['return'] = array(
@@ -2134,6 +2151,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))) {
     if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
       $_SESSION['return'] = array(
       $_SESSION['return'] = array(
         'type' => 'danger',
         'type' => 'danger',
@@ -2164,7 +2189,7 @@ function edit_domain_admin($postarray) {
       return false;
       return false;
     }
     }
 
 
-    if(isset($postarray['domain'])) {
+    if (isset($postarray['domain'])) {
       foreach ($postarray['domain'] as $domain) {
       foreach ($postarray['domain'] as $domain) {
         try {
         try {
           $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
           $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
@@ -2519,6 +2544,14 @@ function mailbox_add_domain($postarray) {
 		return false;
 		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['active'])               ? $active = '1'                 : $active = '0';
 	isset($postarray['relay_all_recipients'])	? $relay_all_recipients = '1'   : $relay_all_recipients = '0';
 	isset($postarray['relay_all_recipients'])	? $relay_all_recipients = '1'   : $relay_all_recipients = '0';
 	isset($postarray['backupmx'])             ? $backupmx = '1'               : $backupmx = '0';
 	isset($postarray['backupmx'])             ? $backupmx = '1'               : $backupmx = '0';
@@ -2623,6 +2656,18 @@ function mailbox_add_alias($postarray) {
 		return false;
 		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) {
 	foreach ($addresses as $address) {
 		if (empty($address)) {
 		if (empty($address)) {
 			continue;
 			continue;
@@ -2632,6 +2677,15 @@ function mailbox_add_alias($postarray) {
 		$local_part   = strstr($address, '@', true);
 		$local_part   = strstr($address, '@', true);
 		$address      = $local_part.'@'.$domain;
 		$address      = $local_part.'@'.$domain;
 
 
+    $domaindata = mailbox_get_domain_details($domain);
+    if ($domaindata['aliases_left'] == 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['max_alias_exceeded'])
+      );
+      return false;
+    }
+      
 		try {
 		try {
 			$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
 			$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
 				WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
 				WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
@@ -3432,7 +3486,7 @@ function mailbox_edit_domain($postarray) {
   // aliases                float
   // aliases                float
   // mailboxes              float
   // mailboxes              float
   // maxquota               float
   // maxquota               float
-  // quota                  float     (Byte)
+  // quota                  float (Byte)
   // active                 int
   // active                 int
 
 
 	global $lang;
 	global $lang;
@@ -3519,6 +3573,14 @@ function mailbox_edit_domain($postarray) {
       return false;
       return false;
     }
     }
 
 
+    if ($maxquota == "0" || empty($maxquota)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['maxquota_empty'])
+      );
+      return false;
+    }
+
     if ($MailboxData['maxquota'] > $maxquota) {
     if ($MailboxData['maxquota'] > $maxquota) {
       $_SESSION['return'] = array(
       $_SESSION['return'] = array(
         'type' => 'danger',
         'type' => 'danger',
@@ -4271,6 +4333,10 @@ function mailbox_get_domain_details($domain) {
       ':domain' => $domain,
       ':domain' => $domain,
     ));
     ));
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    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 = $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']));
     $stmt->execute(array(':domain' => $row['domain']));
     $MailboxDataDomain	= $stmt->fetch(PDO::FETCH_ASSOC);
     $MailboxDataDomain	= $stmt->fetch(PDO::FETCH_ASSOC);
@@ -4303,8 +4369,9 @@ function mailbox_get_domain_details($domain) {
     $stmt->execute(array(
     $stmt->execute(array(
       ':domain' => $domain,
       ':domain' => $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) {
   catch (PDOException $e) {
     $_SESSION['return'] = array(
     $_SESSION['return'] = array(

+ 1 - 2
data/web/inc/header.inc.php

@@ -16,11 +16,10 @@
 <link rel="stylesheet" href="/css/bootstrap-slider.min.css">
 <link rel="stylesheet" href="/css/bootstrap-slider.min.css">
 <link rel="stylesheet" href="/css/bootstrap-switch.min.css">
 <link rel="stylesheet" href="/css/bootstrap-switch.min.css">
 <link rel="stylesheet" href="/css/footable.bootstrap.min.css">
 <link rel="stylesheet" href="/css/footable.bootstrap.min.css">
-<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700&subset=latin,latin-ext">
 <link rel="stylesheet" href="/inc/languages.min.css">
 <link rel="stylesheet" href="/inc/languages.min.css">
 <link rel="stylesheet" href="/css/mailcow.css">
 <link rel="stylesheet" href="/css/mailcow.css">
-<link rel="stylesheet" href="/css/tables.css">
 <?=(preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null;?>
 <?=(preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null;?>
+<?=(preg_match("/admin.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null;?>
 <link rel="shortcut icon" href="/favicon.png" type="image/png">
 <link rel="shortcut icon" href="/favicon.png" type="image/png">
 <link rel="icon" href="/favicon.png" type="image/png">
 <link rel="icon" href="/favicon.png" type="image/png">
 </head>
 </head>

+ 1 - 1
data/web/js/add.js

@@ -2,7 +2,7 @@ $(document).ready(function() {
 	// add.php
 	// add.php
 	// Get max. possible quota for a domain when domain field changes
 	// Get max. possible quota for a domain when domain field changes
 	$('#addSelectDomain').on('change', function() {
 	$('#addSelectDomain').on('change', function() {
-		$.get("json_api.php", { action:"get_domain_details", object:this.value }, function(data){
+		$.get("/api/v1/domain/" + this.value, function(data){
       var result = jQuery.parseJSON( data );
       var result = jQuery.parseJSON( data );
       max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576);
       max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576);
 			if (max_new_mailbox_quota != '0') {
 			if (max_new_mailbox_quota != '0') {

+ 40 - 29
data/web/js/admin.js

@@ -1,31 +1,42 @@
 $(document).ready(function() {
 $(document).ready(function() {
-	// Postfix restrictions, drag and drop functions
-	$( "[id*=srr-sortable]" ).sortable({
-		items: "li:not(.list-heading)",
-		cancel: ".ui-state-disabled",
-		connectWith: "[id*=srr-sortable]",
-		dropOnEmpty: true,
-		placeholder: "ui-state-highlight"
-	});
-	$( "[id*=ssr-sortable]" ).sortable({
-		items: "li:not(.list-heading)",
-		cancel: ".ui-state-disabled",
-		connectWith: "[id*=ssr-sortable]",
-		dropOnEmpty: true,
-		placeholder: "ui-state-highlight"
-	});
-	$('#srr_form').submit(function(){
-		var srr_joined_vals = $("[id^=srr-sortable-active] li").map(function() {
-			return $(this).data("value");
-		}).get().join(', ');
-		var input = $("<input>").attr("type", "hidden").attr("name", "srr_value").val(srr_joined_vals);
-		$('#srr_form').append($(input));
-	});
-	$('#ssr_form').submit(function(){
-		var ssr_joined_vals = $("[id^=ssr-sortable-active] li").map(function() {
-			return $(this).data("value");
-		}).get().join(', ');
-		var input = $("<input>").attr("type", "hidden").attr("name", "ssr_value").val(ssr_joined_vals);
-		$('#ssr_form').append($(input));
-	});
+  $.ajax({
+    dataType: 'json',
+    url: '/api/v1/domain-admin/all',
+    jsonp: false,
+    error: function () {
+      alert('Cannot draw domain administrator table');
+    },
+    success: function (data) {
+      $.each(data, function (i, item) {
+        item.action = '<div class="btn-group">' +
+          '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="/delete.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+					'</div>';
+      });
+      $('#domainadminstable').footable({
+        "columns": [
+          {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}},
+          {"name":"selected_domains","title":lang.admin_domains,"breakpoints":"xs sm"},
+          {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}},
+          {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
+          {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+        ],
+        "rows": data,
+        "empty": lang.empty,
+        "paging": {
+          "enabled": true,
+          "limit": 5,
+          "size": pagination_size
+        },
+        "filtering": {
+          "enabled": true,
+          "position": "left",
+          "placeholder": lang.filter_table
+        },
+        "sorting": {
+          "enabled": true
+        }
+      });
+    }
+  });
 });
 });

+ 11 - 11
data/web/js/mailbox.js

@@ -15,7 +15,7 @@ $(document).ready(function() {
   
   
   $.ajax({
   $.ajax({
     dataType: 'json',
     dataType: 'json',
-    url: '/json_api.php?action=domain_table_data',
+    url: '/api/v1/domain/all',
     jsonp: false,
     jsonp: false,
     error: function () {
     error: function () {
       alert('Cannot draw domain table');
       alert('Cannot draw domain table');
@@ -70,7 +70,7 @@ $(document).ready(function() {
 
 
   $.ajax({
   $.ajax({
     dataType: 'json',
     dataType: 'json',
-    url: '/json_api.php?action=mailbox_table_data',
+    url: '/api/v1/mailbox/all',
     jsonp: false,
     jsonp: false,
     error: function () {
     error: function () {
       alert('Cannot draw mailbox table');
       alert('Cannot draw mailbox table');
@@ -102,12 +102,12 @@ $(document).ready(function() {
           {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}},
           {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}},
           {"name":"name","title":lang.fname,"breakpoints":"xs sm"},
           {"name":"name","title":lang.fname,"breakpoints":"xs sm"},
           {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
           {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
-          {"name":"quota","title":lang.domain_quota},
-          {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm"},
-          {"name":"in_use","filterable": false,"type":"html","title":lang.in_use},
-          {"name":"messages","filterable": false,"style":{"width":"90px"},"title":lang.msg_num,"breakpoints":"xs sm"},
-          {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
-          {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+          {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota},
+          {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"},
+          {"name":"in_use","filterable": false,"style":{"whiteSpace":"nowrap"},"type":"html","title":lang.in_use},
+          {"name":"messages","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.msg_num,"breakpoints":"xs sm md"},
+          {"name":"active","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.active},
+          {"name":"action","filterable": false,"sortable": false,"style":{"whiteSpace":"nowrap","text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
         ],
         ],
         "empty": lang.empty,
         "empty": lang.empty,
         "rows": data,
         "rows": data,
@@ -130,7 +130,7 @@ $(document).ready(function() {
 
 
   $.ajax({
   $.ajax({
     dataType: 'json',
     dataType: 'json',
-    url: '/json_api.php?action=resource_table_data',
+    url: '/api/v1/resource/all',
     jsonp: false,
     jsonp: false,
     error: function () {
     error: function () {
       alert('Cannot draw resource table');
       alert('Cannot draw resource table');
@@ -172,7 +172,7 @@ $(document).ready(function() {
 
 
   $.ajax({
   $.ajax({
     dataType: 'json',
     dataType: 'json',
-    url: '/json_api.php?action=domain_alias_table_data',
+    url: '/api/v1/alias-domain/all',
     jsonp: false,
     jsonp: false,
     error: function () {
     error: function () {
       alert('Cannot draw alias domain table');
       alert('Cannot draw alias domain table');
@@ -212,7 +212,7 @@ $(document).ready(function() {
 
 
   $.ajax({
   $.ajax({
     dataType: 'json',
     dataType: 'json',
-    url: '/json_api.php?action=alias_table_data',
+    url: '/api/v1/alias/all',
     jsonp: false,
     jsonp: false,
     error: function () {
     error: function () {
       alert('Cannot draw alias table');
       alert('Cannot draw alias table');

+ 0 - 236
data/web/js/sorttable.js

@@ -1,236 +0,0 @@
-(function() {
-  var SELECTOR, addEventListener, clickEvents, numberRegExp, sortable, touchDevice, trimRegExp;
-
-  SELECTOR = 'table[data-sortable]';
-
-  numberRegExp = /^-?[£$¤]?[\d,.]+%?$/;
-
-  trimRegExp = /^\s+|\s+$/g;
-
-  clickEvents = ['click'];
-
-  touchDevice = 'ontouchstart' in document.documentElement;
-
-  if (touchDevice) {
-    clickEvents.push('touchstart');
-  }
-
-  addEventListener = function(el, event, handler) {
-    if (el.addEventListener != null) {
-      return el.addEventListener(event, handler, false);
-    } else {
-      return el.attachEvent("on" + event, handler);
-    }
-  };
-
-  sortable = {
-    init: function(options) {
-      var table, tables, _i, _len, _results;
-      if (options == null) {
-        options = {};
-      }
-      if (options.selector == null) {
-        options.selector = SELECTOR;
-      }
-      tables = document.querySelectorAll(options.selector);
-      _results = [];
-      for (_i = 0, _len = tables.length; _i < _len; _i++) {
-        table = tables[_i];
-        _results.push(sortable.initTable(table));
-      }
-      return _results;
-    },
-    initTable: function(table) {
-      var i, th, ths, _i, _len, _ref;
-      if (((_ref = table.tHead) != null ? _ref.rows.length : void 0) !== 1) {
-        return;
-      }
-      if (table.getAttribute('data-sortable-initialized') === 'true') {
-        return;
-      }
-      table.setAttribute('data-sortable-initialized', 'true');
-      ths = table.querySelectorAll('th');
-      for (i = _i = 0, _len = ths.length; _i < _len; i = ++_i) {
-        th = ths[i];
-        if (th.getAttribute('data-sortable') !== 'false') {
-          sortable.setupClickableTH(table, th, i);
-        }
-      }
-      return table;
-    },
-    setupClickableTH: function(table, th, i) {
-      var eventName, onClick, type, _i, _len, _results;
-      type = sortable.getColumnType(table, i);
-      onClick = function(e) {
-        var compare, item, newSortedDirection, position, row, rowArray, sorted, sortedDirection, tBody, ths, value, _compare, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1;
-        if (e.handled !== true) {
-          e.handled = true;
-        } else {
-          return false;
-        }
-        sorted = this.getAttribute('data-sorted') === 'true';
-        sortedDirection = this.getAttribute('data-sorted-direction');
-        if (sorted) {
-          newSortedDirection = sortedDirection === 'ascending' ? 'descending' : 'ascending';
-        } else {
-          newSortedDirection = type.defaultSortDirection;
-        }
-        ths = this.parentNode.querySelectorAll('th');
-        for (_i = 0, _len = ths.length; _i < _len; _i++) {
-          th = ths[_i];
-          th.setAttribute('data-sorted', 'false');
-          th.removeAttribute('data-sorted-direction');
-        }
-        this.setAttribute('data-sorted', 'true');
-        this.setAttribute('data-sorted-direction', newSortedDirection);
-        tBody = table.tBodies[0];
-        rowArray = [];
-        if (!sorted) {
-          if (type.compare != null) {
-            _compare = type.compare;
-          } else {
-            _compare = function(a, b) {
-              return b - a;
-            };
-          }
-          compare = function(a, b) {
-            if (a[0] === b[0]) {
-              return a[2] - b[2];
-            }
-            if (type.reverse) {
-              return _compare(b[0], a[0]);
-            } else {
-              return _compare(a[0], b[0]);
-            }
-          };
-          _ref = tBody.rows;
-          for (position = _j = 0, _len1 = _ref.length; _j < _len1; position = ++_j) {
-            row = _ref[position];
-            value = sortable.getNodeValue(row.cells[i]);
-            if (type.comparator != null) {
-              value = type.comparator(value);
-            }
-            rowArray.push([value, row, position]);
-          }
-          rowArray.sort(compare);
-          for (_k = 0, _len2 = rowArray.length; _k < _len2; _k++) {
-            row = rowArray[_k];
-            tBody.appendChild(row[1]);
-          }
-        } else {
-          _ref1 = tBody.rows;
-          for (_l = 0, _len3 = _ref1.length; _l < _len3; _l++) {
-            item = _ref1[_l];
-            rowArray.push(item);
-          }
-          rowArray.reverse();
-          for (_m = 0, _len4 = rowArray.length; _m < _len4; _m++) {
-            row = rowArray[_m];
-            tBody.appendChild(row);
-          }
-        }
-        if (typeof window['CustomEvent'] === 'function') {
-          return typeof table.dispatchEvent === "function" ? table.dispatchEvent(new CustomEvent('Sortable.sorted', {
-            bubbles: true
-          })) : void 0;
-        }
-      };
-      _results = [];
-      for (_i = 0, _len = clickEvents.length; _i < _len; _i++) {
-        eventName = clickEvents[_i];
-        _results.push(addEventListener(th, eventName, onClick));
-      }
-      return _results;
-    },
-    getColumnType: function(table, i) {
-      var row, specified, text, type, _i, _j, _len, _len1, _ref, _ref1, _ref2;
-      specified = (_ref = table.querySelectorAll('th')[i]) != null ? _ref.getAttribute('data-sortable-type') : void 0;
-      if (specified != null) {
-        return sortable.typesObject[specified];
-      }
-      _ref1 = table.tBodies[0].rows;
-      for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
-        row = _ref1[_i];
-        text = sortable.getNodeValue(row.cells[i]);
-        _ref2 = sortable.types;
-        for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
-          type = _ref2[_j];
-          if (type.match(text)) {
-            return type;
-          }
-        }
-      }
-      return sortable.typesObject.alpha;
-    },
-    getNodeValue: function(node) {
-      var dataValue;
-      if (!node) {
-        return '';
-      }
-      dataValue = node.getAttribute('data-value');
-      if (dataValue !== null) {
-        return dataValue;
-      }
-      if (typeof node.innerText !== 'undefined') {
-        return node.innerText.replace(trimRegExp, '');
-      }
-      return node.textContent.replace(trimRegExp, '');
-    },
-    setupTypes: function(types) {
-      var type, _i, _len, _results;
-      sortable.types = types;
-      sortable.typesObject = {};
-      _results = [];
-      for (_i = 0, _len = types.length; _i < _len; _i++) {
-        type = types[_i];
-        _results.push(sortable.typesObject[type.name] = type);
-      }
-      return _results;
-    }
-  };
-
-  sortable.setupTypes([
-    {
-      name: 'numeric',
-      defaultSortDirection: 'descending',
-      match: function(a) {
-        return a.match(numberRegExp);
-      },
-      comparator: function(a) {
-        return parseFloat(a.replace(/[^0-9.-]/g, ''), 10) || 0;
-      }
-    }, {
-      name: 'date',
-      defaultSortDirection: 'ascending',
-      reverse: true,
-      match: function(a) {
-        return !isNaN(Date.parse(a));
-      },
-      comparator: function(a) {
-        return Date.parse(a) || 0;
-      }
-    }, {
-      name: 'alpha',
-      defaultSortDirection: 'ascending',
-      match: function() {
-        return true;
-      },
-      compare: function(a, b) {
-        return a.localeCompare(b);
-      }
-    }
-  ]);
-
-  setTimeout(sortable.init, 0);
-
-  if (typeof define === 'function' && define.amd) {
-    define(function() {
-      return sortable;
-    });
-  } else if (typeof exports !== 'undefined') {
-    module.exports = sortable;
-  } else {
-    window.Sortable = sortable;
-  }
-
-}).call(this);

+ 218 - 144
data/web/json_api.php

@@ -2,168 +2,242 @@
 require_once 'inc/prerequisites.inc.php';
 require_once 'inc/prerequisites.inc.php';
 error_reporting(E_ALL);
 error_reporting(E_ALL);
 if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
 if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
-  if (isset($_GET['action'])) {
-    $action = $_GET['action'];
+  if (isset($_GET['action']) && isset($_GET['object'])) {
+    $action = filter_input(INPUT_GET, 'action',  FILTER_SANITIZE_STRING);
+    $object   = filter_input(INPUT_GET, 'object',  FILTER_SANITIZE_STRING);
     switch ($action) {
     switch ($action) {
-      case "domain_table_data":
-        $domains = mailbox_get_domains();
-        if (!empty($domains)) {
-          foreach ($domains as $domain) {
-            $data[] = mailbox_get_domain_details($domain);
-          }
-          if (!isset($data) || empty($data)) {
-            echo '{}';
-          }
-          else {
-            echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-          }
-        }
-        else {
-          echo '{}';
-        }
-        break;
-      case "mailbox_table_data":
-        $domains = mailbox_get_domains();
-        if (!empty($domains)) {
-          foreach ($domains as $domain) {
-            $mailboxes = mailbox_get_mailboxes($domain);
-            if (!empty($mailboxes)) {
-              foreach ($mailboxes as $mailbox) {
-                $data[] = mailbox_get_mailbox_details($mailbox);
-              }
-            }
-          }
-          if (!isset($data) || empty($data)) {
-            echo '{}';
-          }
-          else {
-            echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-          }
-        }
-        else {
-          echo '{}';
-        }
-        break;
-      case "resource_table_data":
-        $domains = mailbox_get_domains();
-        if (!empty($domains)) {
-          foreach ($domains as $domain) {
-            $resources = mailbox_get_resources($domain);
-            if (!empty($resources)) {
-              foreach ($resources as $resource) {
-                $data[] = mailbox_get_resource_details($resource);
-              }
-            }
-          }
-          if (!isset($data) || empty($data)) {
-            echo '{}';
-          }
-          else {
-            echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-          }
-        }
-        else {
-          echo '{}';
-        }
-        break;
-      case "domain_alias_table_data":
-        $domains = mailbox_get_domains();
-        if (!empty($domains)) {
-          foreach ($domains as $domain) {
-            $alias_domains = mailbox_get_alias_domains($domain);
-            if (!empty($alias_domains)) {
-              foreach ($alias_domains as $alias_domain) {
-                $data[] = mailbox_get_alias_domain_details($alias_domain);
-              }
-            }
-          }
-          if (!isset($data) || empty($data)) {
-            echo '{}';
-          }
-          else {
-            echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-          }
-        }
-        else {
-          echo '{}';
-        }
-        break;
-      case "alias_table_data":
-        $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains());
-        if (!empty($domains)) {
-          foreach ($domains as $domain) {
-            $aliases = mailbox_get_aliases($domain);
-            if (!empty($aliases)) {
-              foreach ($aliases as $alias) {
-                $data[] = mailbox_get_alias_details($alias);
-              }
-            }
-          }
-          if (!isset($data) || empty($data)) {
-            echo '{}';
-          }
-          else {
-            echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-          }
-        }
-        else {
-          echo '{}';
-        }
-        break;
-      case "get_mailbox_details":
-        if (!isset($_GET['object'])) { return false; }
-        $object = $_GET['object'];
-        $data = mailbox_get_mailbox_details($object);
-        if (!isset($data) || empty($data)) {
-          echo '{}';
-        }
-        else {
-          echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-        }
-        break;
-      case "get_domain_details":
-        if (!isset($_GET['object'])) { return false; }
-        $object = $_GET['object'];
-        $data = mailbox_get_domain_details($object);
-        if (!isset($data) || empty($data)) {
-          echo '{}';
-        }
-        else {
-          echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+      case "domain":
+        switch ($object) {
+          case "all":
+            $domains = mailbox_get_domains();
+            if (!empty($domains)) {
+              foreach ($domains as $domain) {
+                $data[] = mailbox_get_domain_details($domain);
+              }
+              if (!isset($data) || empty($data)) {
+                echo '{}';
+              }
+              else {
+                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+              }
+            }
+            else {
+              echo '{}';
+            }
+          break;
+
+          default:
+            $data = mailbox_get_domain_details($object);
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
+        }
+      break;
+      case "mailbox":
+        switch ($object) {
+          case "all":
+            $domains = mailbox_get_domains();
+            if (!empty($domains)) {
+              foreach ($domains as $domain) {
+                $mailboxes = mailbox_get_mailboxes($domain);
+                if (!empty($mailboxes)) {
+                  foreach ($mailboxes as $mailbox) {
+                    $data[] = mailbox_get_mailbox_details($mailbox);
+                  }
+                }
+              }
+              if (!isset($data) || empty($data)) {
+                echo '{}';
+              }
+              else {
+                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+              }
+            }
+            else {
+              echo '{}';
+            }
+          break;
+
+          default:
+            $data = mailbox_get_mailbox_details($object);
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
+
+        }
+      break;
+      case "resource":
+        switch ($object) {
+          case "all":
+            $domains = mailbox_get_domains();
+            if (!empty($domains)) {
+              foreach ($domains as $domain) {
+                $resources = mailbox_get_resources($domain);
+                if (!empty($resources)) {
+                  foreach ($resources as $resource) {
+                    $data[] = mailbox_get_resource_details($resource);
+                  }
+                }
+              }
+              if (!isset($data) || empty($data)) {
+                echo '{}';
+              }
+              else {
+                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+              }
+            }
+            else {
+              echo '{}';
+            }
+          break;
+
+          default:
+            $data = mailbox_get_resource_details($object);
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode(mailbox_get_resource_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
+
+        }
+      break;
+      case "alias-domain":
+        switch ($object) {
+          case "all":
+            $domains = mailbox_get_domains();
+            if (!empty($domains)) {
+              foreach ($domains as $domain) {
+                $alias_domains = mailbox_get_alias_domains($domain);
+                if (!empty($alias_domains)) {
+                  foreach ($alias_domains as $alias_domain) {
+                    $data[] = mailbox_get_alias_domain_details($alias_domain);
+                  }
+                }
+              }
+              if (!isset($data) || empty($data)) {
+                echo '{}';
+              }
+              else {
+                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+              }
+            }
+            else {
+              echo '{}';
+            }
+          break;
+
+          default:
+            $data = mailbox_get_alias_domains($object);
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode(mailbox_get_alias_domains($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
+        }
+      break;
+      case "alias":
+        switch ($object) {
+          case "all":
+            $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains());
+            if (!empty($domains)) {
+              foreach ($domains as $domain) {
+                $aliases = mailbox_get_aliases($domain);
+                if (!empty($aliases)) {
+                  foreach ($aliases as $alias) {
+                    $data[] = mailbox_get_alias_details($alias);
+                  }
+                }
+              }
+              if (!isset($data) || empty($data)) {
+                echo '{}';
+              }
+              else {
+                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+              }
+            }
+            else {
+              echo '{}';
+            }
+          break;
+
+          default:
+            $data = mailbox_get_alias_details($object);
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode(mailbox_get_alias_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
+        }
+      break;
+      case "domain-admin":
+        switch ($object) {
+          case "all":
+            $domain_admins = get_domain_admins();
+            if (!empty($domain_admins)) {
+              foreach ($domain_admins as $domain_admin) {
+                $data[] = get_domain_admin_details($domain_admin);
+              }
+              if (!isset($data) || empty($data)) {
+                echo '{}';
+              }
+              else {
+                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+              }
+            }
+            else {
+              echo '{}';
+            }
+          break;
+
+          default:
+            $data = get_domain_admin_details($object);
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode(get_domain_admin_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
         }
         }
-        break;
-      case "get_u2f_reg_challenge":
-        if (!isset($_GET['object'])) { return false; }
-        $object = $_GET['object'];
-        if (
-          ($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin")
-          &&
-          ($_SESSION["mailcow_cc_username"] == $object)
-        ) {
+      break;
+      case "u2f-registration":
+        if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) {
           $data = $u2f->getRegisterData(get_u2f_registrations($object));
           $data = $u2f->getRegisterData(get_u2f_registrations($object));
           list($req, $sigs) = $data;
           list($req, $sigs) = $data;
           $_SESSION['regReq'] = json_encode($req);
           $_SESSION['regReq'] = json_encode($req);
           echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';';
           echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';';
         }
         }
         else {
         else {
-          echo '{}';
+          return;
         }
         }
-        break;
-      case "get_u2f_auth_challenge":
-        if (!isset($_GET['object'])) { return false; }
-        $object = $_GET['object'];
+      break;
+      case "u2f-authentication":
         if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
         if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
           $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object)));
           $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object)));
           $_SESSION['authReq']  = $reqs;
           $_SESSION['authReq']  = $reqs;
           echo 'var req = ' . $reqs . ';';
           echo 'var req = ' . $reqs . ';';
         }
         }
         else {
         else {
-          echo '{}';
+          return;
         }
         }
-        break;
+      break;
       default:
       default:
         echo '{}';
         echo '{}';
-        break;
+      break;
     }
     }
   }
   }
 }
 }

+ 9 - 1
data/web/lang/lang.de.php

@@ -36,6 +36,7 @@ $lang['danger']['object_exists'] = 'Objekt %s existiert bereits';
 $lang['danger']['domain_exists'] = 'Domain %s existiert bereits';
 $lang['danger']['domain_exists'] = 'Domain %s existiert bereits';
 $lang['danger']['alias_goto_identical'] = 'Alias- und Ziel-Adresse dürfen nicht identisch sein';
 $lang['danger']['alias_goto_identical'] = 'Alias- und Ziel-Adresse dürfen nicht identisch sein';
 $lang['danger']['aliasd_targetd_identical'] = 'Alias-Domain darf nicht gleich Ziel-Domain sein';
 $lang['danger']['aliasd_targetd_identical'] = 'Alias-Domain darf nicht gleich Ziel-Domain sein';
+$lang['danger']['maxquota_empty'] = 'Max. Speicherplatz pro Mailbox darf nicht 0 sein.';
 $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt';
 $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt';
 $lang['success']['alias_modified'] = 'Änderungen an Alias %s wurden gespeichert';
 $lang['success']['alias_modified'] = 'Änderungen an Alias %s wurden gespeichert';
 $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert';
 $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert';
@@ -70,6 +71,7 @@ $lang['danger']['is_spam_alias'] = '%s lautet bereits eine Spam-Alias-Adresse';
 $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein';
 $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein';
 $lang['danger']['domain_not_found'] = 'Domain "%s" nicht gefunden.';
 $lang['danger']['domain_not_found'] = 'Domain "%s" nicht gefunden.';
 $lang['danger']['max_mailbox_exceeded'] = 'Anzahl an Mailboxen überschritten (%d von %d)';
 $lang['danger']['max_mailbox_exceeded'] = 'Anzahl an Mailboxen überschritten (%d von %d)';
+$lang['danger']['max_alias_exceeded'] = 'Anzahl an Alias-Adressen überschritten';
 $lang['danger']['mailbox_quota_exceeded'] = 'Speicherplatz überschreitet das Limit (max. %d MiB)';
 $lang['danger']['mailbox_quota_exceeded'] = 'Speicherplatz überschreitet das Limit (max. %d MiB)';
 $lang['danger']['mailbox_quota_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)';
 $lang['danger']['mailbox_quota_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)';
 $lang['success']['mailbox_added'] = 'Mailbox %s wurde angelegt';
 $lang['success']['mailbox_added'] = 'Mailbox %s wurde angelegt';
@@ -145,7 +147,7 @@ $lang['user']['spamfilter_default_score'] = 'Standardwert:';
 $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".';
 $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".';
 $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)";
 $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)";
 
 
-$lang['user']['tls_policy_warning'] = '<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br />Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.';
+$lang['user']['tls_policy_warning'] = '<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br />Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.<br />Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox <b>direkt zugeordnet</b> sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse ("Spam-Alias-Adressen"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.';
 $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie';
 $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie';
 $lang['user']['tls_enforce_in'] = 'TLS eingehend erzwingen';
 $lang['user']['tls_enforce_in'] = 'TLS eingehend erzwingen';
 $lang['user']['tls_enforce_out'] = 'TLS ausgehend erzwingen';
 $lang['user']['tls_enforce_out'] = 'TLS ausgehend erzwingen';
@@ -246,6 +248,7 @@ $lang['mailbox']['add_domain_alias'] = 'Domain-Alias hinzufügen';
 $lang['mailbox']['add_mailbox'] = 'Mailbox hinzufügen';
 $lang['mailbox']['add_mailbox'] = 'Mailbox hinzufügen';
 $lang['mailbox']['add_resource'] = 'Ressource hinzufügen';
 $lang['mailbox']['add_resource'] = 'Ressource hinzufügen';
 $lang['mailbox']['add_alias'] = 'Alias hinzufügen';
 $lang['mailbox']['add_alias'] = 'Alias hinzufügen';
+$lang['mailbox']['empty'] = 'Keine Einträge vorhanden';
 
 
 $lang['info']['no_action'] = 'Keine Aktion anwendbar';
 $lang['info']['no_action'] = 'Keine Aktion anwendbar';
 
 
@@ -326,6 +329,9 @@ $lang['add']['subfolder2'] = 'Sync into subfolder on destination';
 $lang['add']['mins_interval'] = 'Abrufintervall (Minuten)';
 $lang['add']['mins_interval'] = 'Abrufintervall (Minuten)';
 $lang['add']['exclude'] = 'Elemente ausschließen (Regex)';
 $lang['add']['exclude'] = 'Elemente ausschließen (Regex)';
 $lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel';
 $lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel';
+$lang['add']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server';
+$lang['edit']['delete2duplicates'] = 'Lösche Duplikate im Ziel';
+$lang['edit']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server';
 
 
 $lang['add']['title'] = 'Objekt anlegen';
 $lang['add']['title'] = 'Objekt anlegen';
 $lang['add']['domain'] = 'Domain';
 $lang['add']['domain'] = 'Domain';
@@ -449,4 +455,6 @@ $lang['admin']['site_not_found'] = 'Kann mailcow Site-Konfiguration nicht finden
 $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; // NEEDS TRANSLATION
 $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; // NEEDS TRANSLATION
 $lang['admin']['set_rr_failed'] = 'Kann Postfix Restriktionen nicht setzen';
 $lang['admin']['set_rr_failed'] = 'Kann Postfix Restriktionen nicht setzen';
 $lang['admin']['no_record'] = 'Kein Eintrag';
 $lang['admin']['no_record'] = 'Kein Eintrag';
+$lang['admin']['filter_table'] = 'Tabelle Filtern';
+$lang['admin']['empty'] = 'Keine Einträge vorhanden';
 ?>
 ?>

+ 9 - 1
data/web/lang/lang.en.php

@@ -38,6 +38,7 @@ $lang['danger']['object_exists'] = "Object %s already exists";
 $lang['danger']['domain_exists'] = "Domain %s already exists";
 $lang['danger']['domain_exists'] = "Domain %s already exists";
 $lang['danger']['alias_goto_identical'] = "Alias and goto address must not be identical";
 $lang['danger']['alias_goto_identical'] = "Alias and goto address must not be identical";
 $lang['danger']['aliasd_targetd_identical'] = "Alias domain must not be equal to target domain";
 $lang['danger']['aliasd_targetd_identical'] = "Alias domain must not be equal to target domain";
+$lang['danger']['maxquota_empty'] = 'Max. quota per mailbox must not be 0.';
 $lang['success']['alias_added'] = "Alias address/es has/have been added";
 $lang['success']['alias_added'] = "Alias address/es has/have been added";
 $lang['success']['alias_modified'] = "Changes to alias have been saved";
 $lang['success']['alias_modified'] = "Changes to alias have been saved";
 $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved";
 $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved";
@@ -72,6 +73,7 @@ $lang['danger']['is_spam_alias'] = "%s is already known as a spam alias address"
 $lang['danger']['quota_not_0_not_numeric'] = "Quota must be numeric and >= 0";
 $lang['danger']['quota_not_0_not_numeric'] = "Quota must be numeric and >= 0";
 $lang['danger']['domain_not_found'] = "Domain not found.";
 $lang['danger']['domain_not_found'] = "Domain not found.";
 $lang['danger']['max_mailbox_exceeded'] = "Max. mailboxes exceeded (%d of %d)";
 $lang['danger']['max_mailbox_exceeded'] = "Max. mailboxes exceeded (%d of %d)";
+$lang['danger']['max_alias_exceeded'] = 'Max. aliases exceeded';
 $lang['danger']['mailbox_quota_exceeded'] = "Quota exceeds the domain limit (max. %d MiB)";
 $lang['danger']['mailbox_quota_exceeded'] = "Quota exceeds the domain limit (max. %d MiB)";
 $lang['danger']['mailbox_quota_left_exceeded'] = "Not enough space left (space left: %d MiB)";
 $lang['danger']['mailbox_quota_left_exceeded'] = "Not enough space left (space left: %d MiB)";
 $lang['success']['mailbox_added'] = "Mailbox %s has been added";
 $lang['success']['mailbox_added'] = "Mailbox %s has been added";
@@ -147,7 +149,7 @@ $lang['user']['spamfilter_default_score'] = 'Default values:';
 $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".';
 $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".';
 $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)";
 $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)";
 
 
-$lang['user']['tls_policy_warning'] = '<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br />Messages to not satisfy the policy will be bounced with a hard fail by the mail system.';
+$lang['user']['tls_policy_warning'] = '<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br />Messages to not satisfy the policy will be bounced with a hard fail by the mail system.<br />This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.';
 $lang['user']['tls_policy'] = 'Encryption policy';
 $lang['user']['tls_policy'] = 'Encryption policy';
 $lang['user']['tls_enforce_in'] = 'Enforce TLS incoming';
 $lang['user']['tls_enforce_in'] = 'Enforce TLS incoming';
 $lang['user']['tls_enforce_out'] = 'Enforce TLS outgoing';
 $lang['user']['tls_enforce_out'] = 'Enforce TLS outgoing';
@@ -249,6 +251,7 @@ $lang['mailbox']['add_mailbox'] = 'Add mailbox';
 $lang['mailbox']['add_resource'] = 'Add resource';
 $lang['mailbox']['add_resource'] = 'Add resource';
 $lang['mailbox']['add_alias'] = 'Add alias';
 $lang['mailbox']['add_alias'] = 'Add alias';
 $lang['mailbox']['add_domain_record_first'] = 'Please add a domain first';
 $lang['mailbox']['add_domain_record_first'] = 'Please add a domain first';
+$lang['mailbox']['empty'] = 'No results';
 
 
 $lang['info']['no_action'] = 'No action applicable';
 $lang['info']['no_action'] = 'No action applicable';
 
 
@@ -330,6 +333,9 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot
 $lang['add']['subfolder2'] = 'Sync into subfolder on destination';
 $lang['add']['subfolder2'] = 'Sync into subfolder on destination';
 $lang['add']['exclude'] = 'Exclude objects (regex)';
 $lang['add']['exclude'] = 'Exclude objects (regex)';
 $lang['add']['delete2duplicates'] = 'Delete duplicates on destination';
 $lang['add']['delete2duplicates'] = 'Delete duplicates on destination';
+$lang['add']['delete1'] = 'Delete from source when completed';
+$lang['edit']['delete2duplicates'] = 'Delete duplicates on destination';
+$lang['edit']['delete1'] = 'Delete from source when completed';
 
 
 $lang['add']['title'] = 'Add object';
 $lang['add']['title'] = 'Add object';
 $lang['add']['domain'] = 'Domain';
 $lang['add']['domain'] = 'Domain';
@@ -460,4 +466,6 @@ $lang['admin']['site_not_found'] = 'Cannot locate mailcow site configuration';
 $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty';
 $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty';
 $lang['admin']['set_rr_failed'] = 'Cannot set Postfix restrictions';
 $lang['admin']['set_rr_failed'] = 'Cannot set Postfix restrictions';
 $lang['admin']['no_record'] = 'No record';
 $lang['admin']['no_record'] = 'No record';
+$lang['admin']['filter_table'] = 'Filter table';
+$lang['admin']['empty'] = 'No results';
 ?>
 ?>

+ 1 - 0
data/web/lang/lang.ru.php

@@ -321,6 +321,7 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot
 $lang['add']['subfolder2'] = "Синхронизировать в подпапку по назначению";
 $lang['add']['subfolder2'] = "Синхронизировать в подпапку по назначению";
 $lang['add']['exclude'] = "Исключить объекты (regex)";
 $lang['add']['exclude'] = "Исключить объекты (regex)";
 $lang['add']['delete2duplicates'] = "Удалить дубликаты в получателях";
 $lang['add']['delete2duplicates'] = "Удалить дубликаты в получателях";
+$lang['edit']['delete2duplicates'] = "Удалить дубликаты в получателях";
 $lang['add']['title'] = "Добавить объект";
 $lang['add']['title'] = "Добавить объект";
 $lang['add']['domain'] = "Домен";
 $lang['add']['domain'] = "Домен";
 $lang['add']['active'] = "Активный";
 $lang['add']['active'] = "Активный";

+ 8 - 26
data/web/mailbox.php

@@ -5,33 +5,11 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 require_once "inc/header.inc.php";
 require_once "inc/header.inc.php";
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 ?>
 ?>
-<style>
-table.footable>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;
-}
-.footer-add-item {
-  text-align:center;
-  font-style: italic;
-  display:block;
-  padding: 10px;
-}
-</style>
 <div class="container">
 <div class="container">
 	<div class="row">
 	<div class="row">
 		<div class="col-md-12">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 			<div class="panel panel-default">
 				<div class="panel-heading">
 				<div class="panel-heading">
-				<h3 class="panel-title"><?=$lang['mailbox']['domains'];?></h3>
 				<div class="pull-right">
 				<div class="pull-right">
 				<?php
 				<?php
 				if ($_SESSION['mailcow_cc_role'] == "admin"):
 				if ($_SESSION['mailcow_cc_role'] == "admin"):
@@ -41,6 +19,7 @@ table.footable>tbody>tr.footable-empty>td {
 				endif;
 				endif;
 				?>
 				?>
 				</div>
 				</div>
+        <h3 class="panel-title"><?=$lang['mailbox']['domains'];?></h3>
 				</div>
 				</div>
         <div class="table-responsive">
         <div class="table-responsive">
           <table id="domain_table" class="table table-striped"></table>
           <table id="domain_table" class="table table-striped"></table>
@@ -49,14 +28,15 @@ table.footable>tbody>tr.footable-empty>td {
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>
+
 	<div class="row">
 	<div class="row">
 		<div class="col-md-12">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 			<div class="panel panel-default">
 				<div class="panel-heading">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['mailboxes'];?></h3>
 					<div class="pull-right">
 					<div class="pull-right">
 						<a href="/add.php?mailbox"><span class="glyphicon glyphicon-plus"></span></a>
 						<a href="/add.php?mailbox"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['mailboxes'];?></h3>
 				</div>
 				</div>
         <div class="table-responsive">
         <div class="table-responsive">
           <table id="mailbox_table" class="table table-striped"></table>
           <table id="mailbox_table" class="table table-striped"></table>
@@ -65,14 +45,15 @@ table.footable>tbody>tr.footable-empty>td {
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>
+
 	<div class="row">
 	<div class="row">
 		<div class="col-md-12">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 			<div class="panel panel-default">
 				<div class="panel-heading">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['resources'];?></h3>
 					<div class="pull-right">
 					<div class="pull-right">
 						<a href="/add.php?resource"><span class="glyphicon glyphicon-plus"></span></a>
 						<a href="/add.php?resource"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['resources'];?></h3>
 				</div>
 				</div>
         <div class="table-responsive">
         <div class="table-responsive">
           <table id="resources_table" class="table table-striped"></table>
           <table id="resources_table" class="table table-striped"></table>
@@ -80,14 +61,15 @@ table.footable>tbody>tr.footable-empty>td {
         <span class="footer-add-item"><a href="/add.php?resource"><?=$lang['mailbox']['add_resource'];?></a></span>			</div>
         <span class="footer-add-item"><a href="/add.php?resource"><?=$lang['mailbox']['add_resource'];?></a></span>			</div>
 		</div>
 		</div>
 	</div>
 	</div>
+
 	<div class="row">
 	<div class="row">
 		<div class="col-md-12">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 			<div class="panel panel-default">
 				<div class="panel-heading">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['domain_aliases'];?></h3>
 					<div class="pull-right">
 					<div class="pull-right">
 						<a href="/add.php?aliasdomain"><span class="glyphicon glyphicon-plus"></span></a>
 						<a href="/add.php?aliasdomain"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['domain_aliases'];?></h3>
 				</div>
 				</div>
         <div class="table-responsive">
         <div class="table-responsive">
           <table id="aliasdomain_table" class="table table-striped"></table>
           <table id="aliasdomain_table" class="table table-striped"></table>
@@ -100,10 +82,10 @@ table.footable>tbody>tr.footable-empty>td {
 		<div class="col-md-12">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 			<div class="panel panel-default">
 				<div class="panel-heading">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['aliases'];?></h3>
 					<div class="pull-right">
 					<div class="pull-right">
 						<a href="/add.php?alias"><span class="glyphicon glyphicon-plus"></span></a>
 						<a href="/add.php?alias"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['aliases'];?></h3>
 				</div>
 				</div>
         <div class="table-responsive">
         <div class="table-responsive">
           <table id="alias_table" class="table table-striped"></table>
           <table id="alias_table" class="table table-striped"></table>

+ 2 - 2
data/web/user.php

@@ -405,7 +405,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 	</div>
 	</div>
 	<div role="tabpanel" class="tab-pane" id="Syncjobs">
 	<div role="tabpanel" class="tab-pane" id="Syncjobs">
 		<div class="table-responsive">
 		<div class="table-responsive">
-		<table class="table table-striped sortable-theme-bootstrap" data-sortable id="timelimitedaliases">
+		<table class="table table-striped" id="timelimitedaliases">
 			<thead>
 			<thead>
 			<tr>
 			<tr>
 				<th class="sort-table" style="min-width: 96px;">Server:Port</th>
 				<th class="sort-table" style="min-width: 96px;">Server:Port</th>
@@ -416,7 +416,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 				<th class="sort-table" style="min-width: 35px;"><?=$lang['user']['last_run'];?></th>
 				<th class="sort-table" style="min-width: 35px;"><?=$lang['user']['last_run'];?></th>
 				<th class="sort-table" style="min-width: 35px;">Log</th>
 				<th class="sort-table" style="min-width: 35px;">Log</th>
 				<th class="sort-table" style="max-width: 95px;"><?=$lang['user']['active'];?></th>
 				<th class="sort-table" style="max-width: 95px;"><?=$lang['user']['active'];?></th>
-				<th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['user']['action'];?></th>
+				<th style="text-align: right; min-width: 200px;"><?=$lang['user']['action'];?></th>
 			</tr>
 			</tr>
 			</thead>
 			</thead>
 			<tbody>
 			<tbody>

+ 4 - 1
docker-compose.yml

@@ -151,10 +151,11 @@ services:
       depends_on:
       depends_on:
         - bind9-mailcow
         - bind9-mailcow
       volumes:
       volumes:
-        - ./data/conf/dovecot:/etc/dovecot
+        - ./data/conf/dovecot:/usr/local/etc/dovecot
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - ./data/conf/sogo/:/etc/sogo/
         - ./data/conf/sogo/:/etc/sogo/
         - vmail-vol-1:/var/vmail
         - vmail-vol-1:/var/vmail
+        - crypt-vol-1:/mail_crypt/
       environment:
       environment:
         - DBNAME=${DBNAME}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBUSER=${DBUSER}
@@ -184,6 +185,7 @@ services:
         - ./data/conf/postfix:/opt/postfix/conf
         - ./data/conf/postfix:/opt/postfix/conf
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - postfix-vol-1:/var/spool/postfix
         - postfix-vol-1:/var/spool/postfix
+        - crypt-vol-1:/var/lib/zeyple
       environment:
       environment:
         - DBNAME=${DBNAME}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBUSER=${DBUSER}
@@ -266,3 +268,4 @@ volumes:
   redis-vol-1:
   redis-vol-1:
   rspamd-vol-1:
   rspamd-vol-1:
   postfix-vol-1:
   postfix-vol-1:
+  crypt-vol-1:

+ 33 - 0
docs/first_steps.md

@@ -136,6 +136,39 @@ server {
 }
 }
 ```
 ```
 
 
+## Optional: Setup a relayhost
+
+Insert these lines to `data/conf/postfix/main.cf`. "relayhost" does already exist (empty), just change its value.
+```
+relayhost = [your-relayhost]:587
+smtp_sasl_password_maps = hash:/opt/postfix/conf/smarthost_passwd
+smtp_sasl_auth_enable = yes
+```
+
+Create the credentials file:
+```
+echo "your-relayhost username:password" > data/conf/postfix/smarthost_passwd
+```
+
+Run:
+```
+docker-compose exec postfix-mailcow postmap /opt/postfix/conf/smarthost_passwd
+docker-compose exec postfix-mailcow chown root:postfix /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db
+docker-compose exec postfix-mailcow chmod 660 /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db
+docker-compose exec postfix-mailcow postfix reload
+```
+
+
+## Install a local MTA
+
+The easiest option would be to disable the listener on port 25/tcp.
+
+**Postfix** users disable the listener by commenting the following line (starting with `smtp` or `25`) in `/etc/postfix/master.cf`:
+```
+#smtp      inet  n       -       -       -       -       smtpd
+```
+Restart Postfix after applying your changes.
+
 ## Sender and receiver model
 ## Sender and receiver model
 
 
 When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address.
 When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address.

+ 4 - 0
docs/install.md

@@ -1,5 +1,7 @@
 ## Install mailcow
 ## Install mailcow
 
 
+**WARNING**: Please use Ubuntu 16.04 instead of Debian 8 or [switch to the kernel 4.9 from jessie backports](https://packages.debian.org/jessie-backports/linux-image-amd64) because there is a bug (kernel panic) with the kernel 3.16 when running docker containers with healthchecks! Full details here: [github.com/docker/docker/issues/30402](https://github.com/docker/docker/issues/30402) and [forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448](https://forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448)
+
 You need Docker and Docker Compose.
 You need Docker and Docker Compose.
 
 
 1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/).
 1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/).
@@ -35,6 +37,8 @@ nano mailcow.conf
 ```
 ```
 If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080.
 If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080.
 
 
+You may need to stop an existing pre-installed MTA which blocks port 25/tcp. See [this chapter](https://andryyy.github.io/mailcow-dockerized/first_steps/#install-a-local-mta) to learn how to reconfigure Postfix to run besides mailcow after a successful installation.
+
 5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached:
 5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached:
 ```
 ```
 docker-compose pull
 docker-compose pull

+ 47 - 0
docs/u_and_e.md

@@ -215,6 +215,51 @@ source mailcow.conf
 docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql
 docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql
 ```
 ```
 
 
+### Reset MySQL passwords
+
+Stop the stack by running `docker-compose stop`.
+
+When the containers came to a stop, run this command:
+
+```
+docker-compose run --rm --entrypoint '/bin/sh -c "gosu mysql mysqld --skip-grant-tables & sleep 10 && mysql -hlocalhost -uroot && exit 0"' mysql-mailcow
+```
+
+**1\. Find database name**
+
+```
+MariaDB [(none)]> show databases;
++--------------------+
+| Database           |
++--------------------+
+| information_schema |
+| mailcow_database   | <=====
+| mysql              |
+| performance_schema |
++--------------------+
+4 rows in set (0.00 sec)
+```
+
+**2\. Reset one or more users**
+
+Both "password" and "authentication_string" exist. Currently "password" is used, but better set both.
+
+```
+MariaDB [(none)]> SELECT user FROM mysql.user;
++--------------+
+| user         |
++--------------+
+| mailcow_user | <===== 
+| root         |
++--------------+
+2 rows in set (0.00 sec)
+
+MariaDB [(none)]> FLUSH PRIVILEGES;
+MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('gotr00t'), password = PASSWORD('gotr00t') WHERE User = 'root' AND Host = '%';
+MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('mookuh'), password = PASSWORD('mookuh') WHERE User = 'mailcow' AND Host = '%';
+MariaDB [(none)]> FLUSH PRIVILEGES;
+```
+
 ## Debugging
 ## Debugging
 
 
 You can use `docker-compose logs $service-name` for all containers.
 You can use `docker-compose logs $service-name` for all containers.
@@ -223,6 +268,8 @@ Run `docker-compose logs` for all logs at once.
 
 
 Follow the log output by running docker-compose with `logs -f`.
 Follow the log output by running docker-compose with `logs -f`.
 
 
+Limit the output by calling logs with `--tail=300` like `docker-compose logs --tail=300 mysql-mailcow`.
+
 ## Redirect port 80 to 443
 ## Redirect port 80 to 443
 
 
 Since February the 28th 2017 mailcow does come with port 80 and 443 enabled.
 Since February the 28th 2017 mailcow does come with port 80 and 443 enabled.