Browse Source

Merge pull request #185 from andryyy/dev

Merge from master to dev
André Peters 8 years ago
parent
commit
9633a34f9f
44 changed files with 1022 additions and 682 deletions
  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>
 
 ENV DEBIAN_FRONTEND noninteractive
 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-core \
 	ca-certificates \
 	supervisor \
 	wget \
 	curl \
-	build-essential \
-	autotools-dev \
-	automake \
+	libssl-dev \
 	libauthen-ntlm-perl \
 	libcrypt-ssleay-perl \
 	libdigest-hmac-perl \
@@ -52,36 +49,57 @@ RUN apt-get -y install dovecot-common \
 	make \
 	cpanminus
 
+
+RUN wget https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz -O - | tar xvz  \
+	&& cd dovecot-$DOVECOT_VERSION \
+	&& ./configure --with-mysql --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib \
+	&& make -j3 \
+	&& make install \
+	&& make clean
+
+RUN wget https://pigeonhole.dovecot.org/releases/2.2/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION.tar.gz -O - | tar xvz  \
+	&& cd dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \
+	&& ./configure \
+	&& make -j3 \
+	&& make install \
+	&& make clean
+
 RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf
 RUN cpanm Data::Uniqid Mail::IMAPClient String::Util
 RUN echo '* * * * *   root   /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync
 RUN echo '30 3 * * *   vmail  /usr/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync
 
-WORKDIR /tmp
-
-RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | tar xvz  \
-	&& cd /tmp/dovecot-antispam* \
-	&& ./autogen.sh \
-	&& ./configure --prefix=/usr \
-	&& make \
-	&& make install
-
 COPY ./imapsync /usr/local/bin/imapsync
 COPY ./postlogin.sh /usr/local/bin/postlogin.sh
 COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl
-COPY ./rspamd-pipe /usr/local/bin/rspamd-pipe
+COPY ./report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve
+COPY ./report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve
+COPY ./rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham
+COPY ./rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam
 COPY ./docker-entrypoint.sh /
 COPY ./supervisord.conf /etc/supervisor/supervisord.conf
 
-RUN chmod +x /usr/local/bin/rspamd-pipe
-RUN chmod +x /usr/local/bin/imapsync_cron.pl
+RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \
+	/usr/local/lib/dovecot/sieve/rspamd-pipe-spam \
+	/usr/local/bin/imapsync_cron.pl \
+	/usr/local/bin/postlogin.sh \
+	/usr/local/bin/imapsync
 
-RUN groupadd -g 5000 vmail
-RUN useradd -g vmail -u 5000 vmail -d /var/vmail
+RUN groupadd -g 5000 vmail \
+	&& groupadd -g 401 dovecot \
+    && groupadd -g 402 dovenull \
+	&& useradd -g vmail -u 5000 vmail -d /var/vmail \
+	&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
+	&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull
 
 EXPOSE 24 10001
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 
-RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
+RUN apt-get clean \
+	&& rm -rf /var/lib/apt/lists/* \
+	/tmp/* \
+	/var/tmp/* \
+	/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \
+	/dovecot-$DOVECOT_VERSION

+ 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 "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl
 
-[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
+# Create missing directories
+[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/
+[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
+[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
 
 # Set Dovecot sql config parameters, escape " in db password
 DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 
-cat <<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}"
 map {
   pattern = priv/quota/storage
@@ -27,7 +31,8 @@ map {
 }
 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
 connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}"
 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';
 EOF
 
-[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
-[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
-cat /etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve
-sievec /var/vmail/sieve/global.sieve
-chown -R vmail:vmail /var/vmail/sieve
+# Create global sieve_after script
+cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve
 
+# Check permissions of vmail directory.
 # Do not do this every start-up, it may take a very long time. So we use a stat check here.
 if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi
 
 # Create random master for SOGo sieve features
 RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
 RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
-echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /etc/dovecot/dovecot-master.passwd
+echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd
 echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds
 
+# 401 is user dovecot
+if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then
+	openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
+	openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem
+	chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
+else
+	chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
+fi
+
+# Compile sieve scripts
+sievec /var/vmail/sieve/global.sieve
+sievec /usr/local/lib/dovecot/sieve/report-spam.sieve
+sievec /usr/local/lib/dovecot/sieve/report-ham.sieve
+
+# Fix permissions
+chown -R vmail:vmail /var/vmail/sieve
+
+
 exec "$@"

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

@@ -21,7 +21,7 @@ open my $file, '<', "/etc/sogo/sieve.creds";
 my $creds = <$file>; 
 close $file;
 my ($master_user, $master_pass) = split /:/, $creds;
-my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)");
+my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)");
 $sth->execute();
 my $row;
 
@@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) {
   $delete2duplicates  = @$row[9];
   $maxage             = @$row[10];
   $subfolder2         = @$row[11];
+  $delete1            = @$row[12];
 
   if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
 
@@ -46,11 +47,12 @@ while ($row = $sth->fetchrow_arrayref()) {
 	"--timeout1", "10",
 	"--tmpdir", "/tmp",
 	"--subscribeall",
-	($exclude			eq ""	? () : ("--exclude", $exclude)),
-	($subfolder2		eq ""	? () : ('--subfolder2', $subfolder2)),
-	($maxage			eq "0"	? () : ('--maxage', $maxage)),
+	($exclude eq ""	? () : ("--exclude", $exclude)),
+	($subfolder2 eq ""	? () : ('--subfolder2', $subfolder2)),
+	($maxage eq "0"	? () : ('--maxage', $maxage)),
 	($delete2duplicates	ne "1"	? () : ('--delete2duplicates')),
-	(!defined($enc1)			? () : ($enc1)),
+	($delete1	ne "1"	? () : ('--delete')),
+	(!defined($enc1) ? () : ($enc1)),
 	"--host1", $host1,
 	"--user1", $user1,
 	"--password1", $password1,

+ 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
 
 [program:dovecot]
-command=/usr/sbin/dovecot -F
+command=/usr/local/sbin/dovecot -F
 autorestart=true
 
 [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>
 
 ENV DEBIAN_FRONTEND noninteractive
@@ -19,10 +19,19 @@ RUN apt-get install -y --no-install-recommends supervisor \
 	postfix-pcre \
 	syslog-ng \
 	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
 
+COPY zeyple.py /usr/local/bin/zeyple.py
+COPY zeyple.conf /etc/zeyple.conf
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 COPY postfix.sh /opt/postfix.sh
 

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

@@ -17,7 +17,7 @@ user = ${DBUSER}
 password = ${DBPASS}
 hosts = mysql
 dbname = ${DBNAME}
-query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in';
+query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in';
 EOF
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
@@ -25,7 +25,7 @@ user = ${DBUSER}
 password = ${DBPASS}
 hosts = mysql
 dbname = ${DBNAME}
-query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out';
+query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out';
 EOF
 
 cat <<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()
 EOF
 
+# Reset GPG key permissions
+mkdir -p /var/lib/zeyple/keys
+chmod 700 /var/lib/zeyple/keys
+chown -R 600:600 /var/lib/zeyple/keys
+
+# Fix Postfix permissions
+chgrp -R postdrop /var/spool/postfix/public
+chgrp -R postdrop /var/spool/postfix/maildrop
+postfix set-permissions
+
+# Check Postfix configuration
 postconf -c /opt/postfix/conf
+
 if [[ $? != 0 ]]; then
 	echo "Postfix configuration error, refusing to start."
 	exit 1
 else
 	postfix -c /opt/postfix/conf start
+	supervisorctl restart postfix-maillog
 	sleep 126144000
 fi

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

@@ -12,6 +12,17 @@ command=/opt/postfix.sh
 autorestart=true
 
 [program:postfix-maillog]
-command=/usr/bin/tail -f /var/log/mail.log
-stdout_logfile=/dev/fd/1
+command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log
+stdout_logfile=/dev/stdout
 stdout_logfile_maxbytes=0
+
+[unix_http_server]
+file=/var/tmp/supervisord.sock  
+chmod=0770  
+chown=nobody:nogroup
+
+[supervisorctl]
+serverurl=unix:///var/tmp/supervisord.sock
+
+[rpcinterface:supervisor]
+supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

+ 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>
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 
-RUN dpkg-divert --local --rename --add /sbin/initctl \
-    && ln -sf /bin/true /sbin/initctl \
-    && dpkg-divert --local --rename --add /usr/bin/ischroot \
-    && ln -sf /bin/true /usr/bin/ischroot
-
 RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \
-	&& echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \
+	&& echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \
 	&& apt-get update \
 	&& apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor
 

+ 2 - 7
data/Dockerfiles/rspamd/Dockerfile

@@ -1,16 +1,11 @@
-FROM ubuntu:xenial
+FROM debian:jessie-slim
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 
-RUN dpkg-divert --local --rename --add /sbin/initctl \
-    && ln -sf /bin/true /sbin/initctl \
-    && dpkg-divert --local --rename --add /usr/bin/ischroot \
-    && ln -sf /bin/true /usr/bin/ischroot
-
 RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \
-    && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \
+    && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \
     && apt-get update \
     && apt-get -y install rspamd ca-certificates python-pip
 

+ 6 - 12
data/Dockerfiles/sogo/Dockerfile

@@ -1,17 +1,12 @@
-FROM ubuntu:xenial
+FROM debian:jessie-slim
 MAINTAINER Andre Peters <andre.peters@servercow.de>
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV LC_ALL C
 ENV GOSU_VERSION 1.9
 
-RUN dpkg-divert --local --rename --add /sbin/initctl \
-    && ln -sf /bin/true /sbin/initctl \
-    && dpkg-divert --local --rename --add /usr/bin/ischroot \
-    && ln -sf /bin/true /usr/bin/ischroot
-
 RUN apt-get update \
-	&& apt-get install -y --no-install-recommends apt-transport-https \
+	&& apt-get install -y --no-install-recommends apt-transport-https gnupg \
 		ca-certificates \
 		wget \
 		syslog-ng \
@@ -29,8 +24,11 @@ RUN apt-get update \
     && chmod +x /usr/local/bin/gosu \
     && gosu nobody true
 
+RUN mkdir /usr/share/doc/sogo
+RUN touch /usr/share/doc/sogo/empty.sh
+
 RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \
-	&& echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \
+	&& echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ jessie jessie" > /etc/apt/sources.list.d/sogo.list \
 	&& apt-get update \
 	&& apt-get -y --force-yes install sogo sogo-activesync 
 
@@ -42,10 +40,6 @@ RUN echo '0 0 * * *   sogo   /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s
 COPY ./reconf-domains.sh /
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 
-#EXPOSE 20000
-#EXPOSE 9191
-#EXPOSE 9192
-
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 
 RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

+ 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"
 mail_home = /var/vmail/%d/%n
 mail_location = maildir:~/
-mail_plugins = quota acl zlib antispam
-auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@
-ssl_protocols = !SSLv3 !SSLv2
+mail_plugins = quota acl zlib #mail_crypt
+ssl_protocols = !SSLv3
 ssl_prefer_server_ciphers = yes
 ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA
 ssl_options = no_compression
@@ -24,12 +23,12 @@ auth_master_user_separator = *
 mail_prefetch_count = 30
 passdb {
   driver = passwd-file
-  args = /etc/dovecot/dovecot-master.passwd
+  args = /usr/local/etc/dovecot/dovecot-master.passwd
   master = yes
   pass = yes
 }
 passdb {
-  args = /etc/dovecot/sql/dovecot-mysql.conf
+  args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf
   driver = sql
 }
 namespace inbox {
@@ -202,15 +201,15 @@ listen = *,[::]
 ssl_cert = </etc/ssl/mail/cert.pem
 ssl_key = </etc/ssl/mail/key.pem
 userdb {
-  args = /etc/dovecot/sql/dovecot-mysql.conf
+  args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf
   driver = sql
 }
 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 {
-  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 {
   managesieve_logout_format = bytes=%i/%o
@@ -221,22 +220,31 @@ plugin {
   acl = vfile
   quota = dict:Userquota::proxy::sqlquota
   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_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_max_script_size = 1M
   sieve_quota_max_scripts = 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 {
-  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 {
   disable_plaintext_auth = no

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

@@ -18,6 +18,11 @@ server {
   access_log /var/log/nginx/access.log;
   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/ {
 	  allow all;
     default_type "text/plain";
@@ -166,6 +171,11 @@ server {
   access_log /var/log/nginx/access.log;
   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/ {
 	  allow all;
     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
 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
 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 syslog_name=enforced-tls-smtp
   -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
+
 tlsproxy   unix  -       -       n       -       0       tlsproxy
 dnsblog    unix  -       -       n       -       0       dnsblog
 pickup     fifo  n       -       n       60      1       pickup
@@ -43,3 +44,14 @@ anvil      unix  -       -       n       -       1       anvil
 scache     unix  -       -       n       -       1       scache
 maildrop   unix  -       n       n       -       -       pipe flags=DRhu
     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 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="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">

+ 14 - 8
data/web/admin.php

@@ -81,14 +81,14 @@ $tfa_data = get_tfa();
         <div class="panel-body">
           <form method="post">
             <div class="table-responsive">
-            <table class="table table-striped sortable-theme-bootstrap" data-sortable id="domainadminstable">
+            <table class="table table-striped" id="domainadminstable">
               <thead>
               <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>
               </thead>
               <tbody>
@@ -299,8 +299,14 @@ $tfa_data = get_tfa();
   </div>
   </div>
 </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>
 <?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');?>">
 						</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="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">

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

@@ -50,11 +50,7 @@ $(document).ready(function() {
           type: "GET",
           cache: false,
           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){
             data;
           }
@@ -87,11 +83,7 @@ $(document).ready(function() {
         type: "GET",
         cache: false,
         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){
           data;
         }

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

@@ -109,6 +109,11 @@ function init_db_schema() {
   if ($num_results == 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'");
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
   if ($num_results == 0) {
@@ -1075,6 +1080,7 @@ function add_syncjob($postarray) {
   }
   isset($postarray['active']) ? $active = '1' : $active = '0';
   isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+  isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0';
   $port1            = $postarray['port1'];
   $host1            = $postarray['host1'];
   $password1        = $postarray['password1'];
@@ -1147,12 +1153,13 @@ function add_syncjob($postarray) {
     return false;
   }
   try {
-    $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`)
-      VALUES (:user2, :exclude, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)");
+    $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`)
+      VALUES (:user2, :exclude, :maxage, :delete1, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)");
     $stmt->execute(array(
       ':user2' => $username,
       ':exclude' => $exclude,
       ':maxage' => $maxage,
+      ':delete1' => $delete1,
       ':subfolder2' => $subfolder2,
       ':host1' => $host1,
       ':authmech1' => 'PLAIN',
@@ -1200,6 +1207,7 @@ function edit_syncjob($postarray) {
   }
   isset($postarray['active']) ? $active = '1' : $active = '0';
   isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+  isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0';
   $id               = $postarray['id'];
   $port1            = $postarray['port1'];
   $host1            = $postarray['host1'];
@@ -1273,10 +1281,11 @@ function edit_syncjob($postarray) {
     return false;
   }
   try {
-    $stmt = $pdo->prepare("UPDATE `imapsync` set `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
+    $stmt = $pdo->prepare("UPDATE `imapsync` set `delete1` = :delete1, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
       WHERE `user2` = :user2 AND `id` = :id");
     $stmt->execute(array(
       ':user2' => $username,
+      ':delete1' => $delete1,
       ':id' => $id,
       ':exclude' => $exclude,
       ':maxage' => $maxage,
@@ -1757,6 +1766,7 @@ function get_domain_admin_details($domain_admin) {
   try {
     $stmt = $pdo->prepare("SELECT
       `tfa`.`active` AS `tfa_active_int`,
+      CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`,
       `domain_admins`.`username`,
       `domain_admins`.`created`,
       `domain_admins`.`active` AS `active_int`,
@@ -1768,11 +1778,15 @@ function get_domain_admin_details($domain_admin) {
       ':domain_admin' => $domain_admin
     ));
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (empty($row)) { 
+      return false;
+    }
     $domainadmindata['username'] = $row['username'];
+    $domainadmindata['tfa_active'] = $row['tfa_active'];
     $domainadmindata['active'] = $row['active'];
-    $domainadmindata['active_int'] = $row['active_int'];
     $domainadmindata['tfa_active_int'] = $row['tfa_active_int'];
-    $domainadmindata['created'] = $row['created'];
+    $domainadmindata['active_int'] = $row['active_int'];
+    $domainadmindata['modified'] = $row['created'];
     // GET SELECTED
     $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
       WHERE `domain` IN (
@@ -1793,6 +1807,9 @@ function get_domain_admin_details($domain_admin) {
     while($row = array_shift($rows)) {
       $domainadmindata['unselected_domains'][] = $row['domain'];
     }
+    if (!isset($domainadmindata['unselected_domains'])) {
+      $domainadmindata['unselected_domains'] = "";
+    }
   }
   catch(PDOException $e) {
     $_SESSION['return'] = array(
@@ -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))) {
       $_SESSION['return'] = array(
         'type' => 'danger',
@@ -2164,7 +2189,7 @@ function edit_domain_admin($postarray) {
       return false;
     }
 
-    if(isset($postarray['domain'])) {
+    if (isset($postarray['domain'])) {
       foreach ($postarray['domain'] as $domain) {
         try {
           $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
@@ -2519,6 +2544,14 @@ function mailbox_add_domain($postarray) {
 		return false;
 	}
 
+	if ($maxquota == "0" || empty($maxquota)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['maxquota_empty'])
+		);
+		return false;
+	}
+
 	isset($postarray['active'])               ? $active = '1'                 : $active = '0';
 	isset($postarray['relay_all_recipients'])	? $relay_all_recipients = '1'   : $relay_all_recipients = '0';
 	isset($postarray['backupmx'])             ? $backupmx = '1'               : $backupmx = '0';
@@ -2623,6 +2656,18 @@ function mailbox_add_alias($postarray) {
 		return false;
 	}
 
+  $stmt = $pdo->prepare("SELECT `address` FROM `alias`
+    WHERE `address`= :address");
+  $stmt->execute(array(':address' => $address));
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results != 0) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address))
+    );
+    return false;
+  }
+
 	foreach ($addresses as $address) {
 		if (empty($address)) {
 			continue;
@@ -2632,6 +2677,15 @@ function mailbox_add_alias($postarray) {
 		$local_part   = strstr($address, '@', true);
 		$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 {
 			$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
 				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
   // mailboxes              float
   // maxquota               float
-  // quota                  float     (Byte)
+  // quota                  float (Byte)
   // active                 int
 
 	global $lang;
@@ -3519,6 +3573,14 @@ function mailbox_edit_domain($postarray) {
       return false;
     }
 
+    if ($maxquota == "0" || empty($maxquota)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['maxquota_empty'])
+      );
+      return false;
+    }
+
     if ($MailboxData['maxquota'] > $maxquota) {
       $_SESSION['return'] = array(
         'type' => 'danger',
@@ -4271,6 +4333,10 @@ function mailbox_get_domain_details($domain) {
       ':domain' => $domain,
     ));
     $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->execute(array(':domain' => $row['domain']));
     $MailboxDataDomain	= $stmt->fetch(PDO::FETCH_ASSOC);
@@ -4303,8 +4369,9 @@ function mailbox_get_domain_details($domain) {
     $stmt->execute(array(
       ':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) {
     $_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-switch.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="/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("/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="icon" href="/favicon.png" type="image/png">
 </head>

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

@@ -2,7 +2,7 @@ $(document).ready(function() {
 	// add.php
 	// Get max. possible quota for a domain when domain field changes
 	$('#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 );
       max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576);
 			if (max_new_mailbox_quota != '0') {

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

@@ -1,31 +1,42 @@
 $(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({
     dataType: 'json',
-    url: '/json_api.php?action=domain_table_data',
+    url: '/api/v1/domain/all',
     jsonp: false,
     error: function () {
       alert('Cannot draw domain table');
@@ -70,7 +70,7 @@ $(document).ready(function() {
 
   $.ajax({
     dataType: 'json',
-    url: '/json_api.php?action=mailbox_table_data',
+    url: '/api/v1/mailbox/all',
     jsonp: false,
     error: function () {
       alert('Cannot draw mailbox table');
@@ -102,12 +102,12 @@ $(document).ready(function() {
           {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}},
           {"name":"name","title":lang.fname,"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,
         "rows": data,
@@ -130,7 +130,7 @@ $(document).ready(function() {
 
   $.ajax({
     dataType: 'json',
-    url: '/json_api.php?action=resource_table_data',
+    url: '/api/v1/resource/all',
     jsonp: false,
     error: function () {
       alert('Cannot draw resource table');
@@ -172,7 +172,7 @@ $(document).ready(function() {
 
   $.ajax({
     dataType: 'json',
-    url: '/json_api.php?action=domain_alias_table_data',
+    url: '/api/v1/alias-domain/all',
     jsonp: false,
     error: function () {
       alert('Cannot draw alias domain table');
@@ -212,7 +212,7 @@ $(document).ready(function() {
 
   $.ajax({
     dataType: 'json',
-    url: '/json_api.php?action=alias_table_data',
+    url: '/api/v1/alias/all',
     jsonp: false,
     error: function () {
       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';
 error_reporting(E_ALL);
 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) {
-      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));
           list($req, $sigs) = $data;
           $_SESSION['regReq'] = json_encode($req);
           echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';';
         }
         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) {
           $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object)));
           $_SESSION['authReq']  = $reqs;
           echo 'var req = ' . $reqs . ';';
         }
         else {
-          echo '{}';
+          return;
         }
-        break;
+      break;
       default:
         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']['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']['maxquota_empty'] = 'Max. Speicherplatz pro Mailbox darf nicht 0 sein.';
 $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt';
 $lang['success']['alias_modified'] = 'Änderungen an Alias %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']['domain_not_found'] = 'Domain "%s" nicht gefunden.';
 $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_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)';
 $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_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_enforce_in'] = 'TLS eingehend 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_resource'] = 'Ressource hinzufügen';
 $lang['mailbox']['add_alias'] = 'Alias hinzufügen';
+$lang['mailbox']['empty'] = 'Keine Einträge vorhanden';
 
 $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']['exclude'] = 'Elemente ausschließen (Regex)';
 $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']['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']['set_rr_failed'] = 'Kann Postfix Restriktionen nicht setzen';
 $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']['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']['maxquota_empty'] = 'Max. quota per mailbox must not be 0.';
 $lang['success']['alias_added'] = "Alias address/es has/have been added";
 $lang['success']['alias_modified'] = "Changes to alias 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']['domain_not_found'] = "Domain not found.";
 $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_left_exceeded'] = "Not enough space left (space left: %d MiB)";
 $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_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_enforce_in'] = 'Enforce TLS incoming';
 $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_alias'] = 'Add alias';
 $lang['mailbox']['add_domain_record_first'] = 'Please add a domain first';
+$lang['mailbox']['empty'] = 'No results';
 
 $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']['exclude'] = 'Exclude objects (regex)';
 $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']['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']['set_rr_failed'] = 'Cannot set Postfix restrictions';
 $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']['exclude'] = "Исключить объекты (regex)";
 $lang['add']['delete2duplicates'] = "Удалить дубликаты в получателях";
+$lang['edit']['delete2duplicates'] = "Удалить дубликаты в получателях";
 $lang['add']['title'] = "Добавить объект";
 $lang['add']['domain'] = "Домен";
 $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";
 $_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="row">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 				<div class="panel-heading">
-				<h3 class="panel-title"><?=$lang['mailbox']['domains'];?></h3>
 				<div class="pull-right">
 				<?php
 				if ($_SESSION['mailcow_cc_role'] == "admin"):
@@ -41,6 +19,7 @@ table.footable>tbody>tr.footable-empty>td {
 				endif;
 				?>
 				</div>
+        <h3 class="panel-title"><?=$lang['mailbox']['domains'];?></h3>
 				</div>
         <div class="table-responsive">
           <table id="domain_table" class="table table-striped"></table>
@@ -49,14 +28,15 @@ table.footable>tbody>tr.footable-empty>td {
 			</div>
 		</div>
 	</div>
+
 	<div class="row">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['mailboxes'];?></h3>
 					<div class="pull-right">
 						<a href="/add.php?mailbox"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['mailboxes'];?></h3>
 				</div>
         <div class="table-responsive">
           <table id="mailbox_table" class="table table-striped"></table>
@@ -65,14 +45,15 @@ table.footable>tbody>tr.footable-empty>td {
 			</div>
 		</div>
 	</div>
+
 	<div class="row">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['resources'];?></h3>
 					<div class="pull-right">
 						<a href="/add.php?resource"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['resources'];?></h3>
 				</div>
         <div class="table-responsive">
           <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>
 		</div>
 	</div>
+
 	<div class="row">
 		<div class="col-md-12">
 			<div class="panel panel-default">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['domain_aliases'];?></h3>
 					<div class="pull-right">
 						<a href="/add.php?aliasdomain"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['domain_aliases'];?></h3>
 				</div>
         <div class="table-responsive">
           <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="panel panel-default">
 				<div class="panel-heading">
-					<h3 class="panel-title"><?=$lang['mailbox']['aliases'];?></h3>
 					<div class="pull-right">
 						<a href="/add.php?alias"><span class="glyphicon glyphicon-plus"></span></a>
 					</div>
+					<h3 class="panel-title"><?=$lang['mailbox']['aliases'];?></h3>
 				</div>
         <div class="table-responsive">
           <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 role="tabpanel" class="tab-pane" id="Syncjobs">
 		<div class="table-responsive">
-		<table class="table table-striped sortable-theme-bootstrap" data-sortable id="timelimitedaliases">
+		<table class="table table-striped" id="timelimitedaliases">
 			<thead>
 			<tr>
 				<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;">Log</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>
 			</thead>
 			<tbody>

+ 4 - 1
docker-compose.yml

@@ -151,10 +151,11 @@ services:
       depends_on:
         - bind9-mailcow
       volumes:
-        - ./data/conf/dovecot:/etc/dovecot
+        - ./data/conf/dovecot:/usr/local/etc/dovecot
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - ./data/conf/sogo/:/etc/sogo/
         - vmail-vol-1:/var/vmail
+        - crypt-vol-1:/mail_crypt/
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -184,6 +185,7 @@ services:
         - ./data/conf/postfix:/opt/postfix/conf
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - postfix-vol-1:/var/spool/postfix
+        - crypt-vol-1:/var/lib/zeyple
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -266,3 +268,4 @@ volumes:
   redis-vol-1:
   rspamd-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
 
 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
 
+**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.
 
 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.
 
+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:
 ```
 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
 ```
 
+### 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
 
 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`.
 
+Limit the output by calling logs with `--tail=300` like `docker-compose logs --tail=300 mysql-mailcow`.
+
 ## Redirect port 80 to 443
 
 Since February the 28th 2017 mailcow does come with port 80 and 443 enabled.