diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index abd07c1f..90f7352d 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,33 +1,30 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim +#ubuntu:xenial MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C +ENV DOVECOT_VERSION 2.2.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 diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 8ef09dba..4a2876d7 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/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 < /etc/dovecot/sql/dovecot-dict-sql.conf +# Create quota dict for Dovecot +cat < /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 < /etc/dovecot/sql/dovecot-mysql.conf +# Create user and pass dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = mysql connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" 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 "$@" diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl index 5c47eb47..a3cbf8a1 100755 --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -21,7 +21,7 @@ open my $file, '<', "/etc/sogo/sieve.creds"; my $creds = <$file>; close $file; my ($master_user, $master_pass) = split /:/, $creds; -my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); +my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); $sth->execute(); my $row; @@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) { $delete2duplicates = @$row[9]; $maxage = @$row[10]; $subfolder2 = @$row[11]; + $delete1 = @$row[12]; if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } @@ -46,11 +47,12 @@ while ($row = $sth->fetchrow_arrayref()) { "--timeout1", "10", "--tmpdir", "/tmp", "--subscribeall", - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - (!defined($enc1) ? () : ($enc1)), + ($delete1 ne "1" ? () : ('--delete')), + (!defined($enc1) ? () : ($enc1)), "--host1", $host1, "--user1", $user1, "--password1", $password1, diff --git a/data/Dockerfiles/dovecot/report-ham.sieve b/data/Dockerfiles/dovecot/report-ham.sieve new file mode 100644 index 00000000..80c7f44e --- /dev/null +++ b/data/Dockerfiles/dovecot/report-ham.sieve @@ -0,0 +1,11 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +pipe :copy "rspamd-pipe-ham"; diff --git a/data/Dockerfiles/dovecot/report-spam.sieve b/data/Dockerfiles/dovecot/report-spam.sieve new file mode 100644 index 00000000..d44cb9a7 --- /dev/null +++ b/data/Dockerfiles/dovecot/report-spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy"]; + +pipe :copy "rspamd-pipe-spam"; diff --git a/data/Dockerfiles/dovecot/rspamd-pipe b/data/Dockerfiles/dovecot/rspamd-pipe deleted file mode 100755 index f9236e17..00000000 --- a/data/Dockerfiles/dovecot/rspamd-pipe +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -if [[ ${2} == "learn_spam" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnspam < /dev/stdin -elif [[ ${2} == "learn_ham" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnham < /dev/stdin -fi -# Always return 0 to satisfy Dovecot... -exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham new file mode 100755 index 00000000..7c3ab03f --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnham < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam new file mode 100755 index 00000000..67cccb2c --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnspam < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 45f9ddd5..e5a66f22 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -8,7 +8,7 @@ autostart=true stdout_syslog=true [program:dovecot] -command=/usr/sbin/dovecot -F +command=/usr/local/sbin/dovecot -F autorestart=true [program:logfiles] diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 6a36f443..0fcdc893 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:xenial +FROM debian:testing-slim MAINTAINER Andre Peters 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 diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c628e771..640538b0 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -17,7 +17,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF cat < /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf @@ -25,7 +25,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out'; EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -92,11 +92,24 @@ dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() EOF +# Reset GPG key permissions +mkdir -p /var/lib/zeyple/keys +chmod 700 /var/lib/zeyple/keys +chown -R 600:600 /var/lib/zeyple/keys + +# Fix Postfix permissions +chgrp -R postdrop /var/spool/postfix/public +chgrp -R postdrop /var/spool/postfix/maildrop +postfix set-permissions + +# Check Postfix configuration postconf -c /opt/postfix/conf + if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 else postfix -c /opt/postfix/conf start + supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 4268899d..72523a61 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -12,6 +12,17 @@ command=/opt/postfix.sh autorestart=true [program:postfix-maillog] -command=/usr/bin/tail -f /var/log/mail.log -stdout_logfile=/dev/fd/1 +command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log +stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 + +[unix_http_server] +file=/var/tmp/supervisord.sock +chmod=0770 +chown=nobody:nogroup + +[supervisorctl] +serverurl=unix:///var/tmp/supervisord.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf new file mode 100644 index 00000000..7f039582 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -0,0 +1,9 @@ +[zeyple] +log_file = /var/log/zeyple.log + +[gpg] +home = /var/lib/zeyple/keys + +[relay] +host = localhost +port = 10026 diff --git a/data/Dockerfiles/postfix/zeyple.py b/data/Dockerfiles/postfix/zeyple.py new file mode 100755 index 00000000..bb218831 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import logging +import email +import email.mime.multipart +import email.mime.application +import email.encoders +import smtplib +import copy +from io import BytesIO + +try: + from configparser import SafeConfigParser # Python 3 +except ImportError: + from ConfigParser import SafeConfigParser # Python 2 + +import gpgme + +# Boiler plate to avoid dependency on six +# BBB: Python 2.7 support +PY3K = sys.version_info > (3, 0) + + +def message_from_binary(message): + if PY3K: + return email.message_from_bytes(message) + else: + return email.message_from_string(message) + + +def as_binary_string(email): + if PY3K: + return email.as_bytes() + else: + return email.as_string() + + +def encode_string(string): + if isinstance(string, bytes): + return string + else: + return string.encode('utf-8') + + +__title__ = 'Zeyple' +__version__ = '1.2.0' +__author__ = 'Cédric Félizard' +__license__ = 'AGPLv3+' +__copyright__ = 'Copyright 2012-2016 Cédric Félizard' + + +class Zeyple: + """Zeyple Encrypts Your Precious Log Emails""" + + def __init__(self, config_fname='zeyple.conf'): + self.config = self.load_configuration(config_fname) + + log_file = self.config.get('zeyple', 'log_file') + logging.basicConfig( + filename=log_file, level=logging.DEBUG, + format='%(asctime)s %(process)s %(levelname)s %(message)s' + ) + logging.info("Zeyple ready to encrypt outgoing emails") + + def load_configuration(self, filename): + """Reads and parses the config file""" + + config = SafeConfigParser() + config.read([ + os.path.join('/etc/', filename), + filename, + ]) + if not config.sections(): + raise IOError('Cannot open config file.') + return config + + @property + def gpg(self): + protocol = gpgme.PROTOCOL_OpenPGP + + if self.config.has_option('gpg', 'executable'): + executable = self.config.get('gpg', 'executable') + else: + executable = None # Default value + + home_dir = self.config.get('gpg', 'home') + + ctx = gpgme.Context() + ctx.set_engine_info(protocol, executable, home_dir) + ctx.armor = True + + return ctx + + def process_message(self, message_data, recipients): + """Encrypts the message with recipient keys""" + message_data = encode_string(message_data) + + in_message = message_from_binary(message_data) + logging.info( + "Processing outgoing message %s", in_message['Message-id']) + + if not recipients: + logging.warn("Cannot find any recipients, ignoring") + + sent_messages = [] + for recipient in recipients: + logging.info("Recipient: %s", recipient) + + key_id = self._user_key(recipient) + logging.info("Key ID: %s", key_id) + + if key_id: + out_message = self._encrypt_message(in_message, key_id) + + # Delete Content-Transfer-Encoding if present to default to + # "7bit" otherwise Thunderbird seems to hang in some cases. + del out_message["Content-Transfer-Encoding"] + else: + logging.warn("No keys found, message will be sent unencrypted") + out_message = copy.copy(in_message) + + self._add_zeyple_header(out_message) + self._send_message(out_message, recipient) + sent_messages.append(out_message) + + return sent_messages + + def _get_version_part(self): + ret = email.mime.application.MIMEApplication( + 'Version: 1\n', + 'pgp-encrypted', + email.encoders.encode_noop, + ) + ret.add_header( + 'Content-Description', + "PGP/MIME version identification", + ) + return ret + + def _get_encrypted_part(self, payload): + ret = email.mime.application.MIMEApplication( + payload, + 'octet-stream', + email.encoders.encode_noop, + name="encrypted.asc", + ) + ret.add_header('Content-Description', "OpenPGP encrypted message") + ret.add_header( + 'Content-Disposition', + 'inline', + filename='encrypted.asc', + ) + return ret + + def _encrypt_message(self, in_message, key_id): + if in_message.is_multipart(): + # get the body (after the first \n\n) + payload = in_message.as_string().split("\n\n", 1)[1].strip() + + # prepend the Content-Type including the boundary + content_type = "Content-Type: " + in_message["Content-Type"] + payload = content_type + "\n\n" + payload + + message = email.message.Message() + message.set_payload(payload) + + payload = message.get_payload() + + else: + payload = in_message.get_payload() + payload = encode_string(payload) + + quoted_printable = email.charset.Charset('ascii') + quoted_printable.body_encoding = email.charset.QP + + message = email.mime.nonmultipart.MIMENonMultipart( + 'text', 'plain', charset='utf-8' + ) + message.set_payload(payload, charset=quoted_printable) + + mixed = email.mime.multipart.MIMEMultipart( + 'mixed', + None, + [message], + ) + + # remove superfluous header + del mixed['MIME-Version'] + + payload = as_binary_string(mixed) + + encrypted_payload = self._encrypt_payload(payload, [key_id]) + + version = self._get_version_part() + encrypted = self._get_encrypted_part(encrypted_payload) + + out_message = copy.copy(in_message) + out_message.preamble = "This is an OpenPGP/MIME encrypted " \ + "message (RFC 4880 and 3156)" + + if 'Content-Type' not in out_message: + out_message['Content-Type'] = 'multipart/encrypted' + else: + out_message.replace_header( + 'Content-Type', + 'multipart/encrypted', + ) + + out_message.set_param('protocol', 'application/pgp-encrypted') + out_message.set_payload([version, encrypted]) + + return out_message + + def _encrypt_payload(self, payload, key_ids): + """Encrypts the payload with the given keys""" + payload = encode_string(payload) + + plaintext = BytesIO(payload) + ciphertext = BytesIO() + + self.gpg.armor = True + + recipient = [self.gpg.get_key(key_id) for key_id in key_ids] + + self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) + + return ciphertext.getvalue() + + def _user_key(self, email): + """Returns the GPG key for the given email address""" + logging.info("Trying to encrypt for %s", email) + keys = [key for key in self.gpg.keylist(email)] + + if keys: + key = keys.pop() # NOTE: looks like keys[0] is the master key + key_id = key.subkeys[0].keyid + return key_id + + return None + + def _add_zeyple_header(self, message): + if self.config.has_option('zeyple', 'add_header') and \ + self.config.getboolean('zeyple', 'add_header'): + message.add_header( + 'X-Zeyple', + "processed by {0} v{1}".format(__title__, __version__) + ) + + def _send_message(self, message, recipient): + """Sends the given message through the SMTP relay""" + logging.info("Sending message %s", message['Message-id']) + + smtp = smtplib.SMTP(self.config.get('relay', 'host'), + self.config.get('relay', 'port')) + + smtp.sendmail(message['From'], recipient, message.as_string()) + smtp.quit() + + logging.info("Message %s sent", message['Message-id']) + + +if __name__ == '__main__': + recipients = sys.argv[1:] + + # BBB: Python 2.7 support + binary_stdin = sys.stdin.buffer if PY3K else sys.stdin + message = binary_stdin.read() + + zeyple = Zeyple() + zeyple.process_message(message, recipients) diff --git a/data/Dockerfiles/rmilter/Dockerfile b/data/Dockerfiles/rmilter/Dockerfile index 364c78c1..1d5db5b0 100644 --- a/data/Dockerfiles/rmilter/Dockerfile +++ b/data/Dockerfiles/rmilter/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 70a0bbab..46a97748 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get -y install rspamd ca-certificates python-pip diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 43960438..348231de 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,17 +1,12 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C ENV GOSU_VERSION 1.9 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-transport-https \ + && apt-get install -y --no-install-recommends apt-transport-https gnupg \ ca-certificates \ wget \ syslog-ng \ @@ -29,8 +24,11 @@ RUN apt-get update \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true +RUN mkdir /usr/share/doc/sogo +RUN touch /usr/share/doc/sogo/empty.sh + RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \ - && echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \ + && echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ jessie jessie" > /etc/apt/sources.list.d/sogo.list \ && apt-get update \ && apt-get -y --force-yes install sogo sogo-activesync @@ -42,10 +40,6 @@ RUN echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s COPY ./reconf-domains.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf -#EXPOSE 20000 -#EXPOSE 9191 -#EXPOSE 9192 - CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 1d05571e..b4501e1a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,9 +10,8 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib antispam -auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -ssl_protocols = !SSLv3 !SSLv2 +mail_plugins = quota acl zlib #mail_crypt +ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA ssl_options = no_compression @@ -24,12 +23,12 @@ auth_master_user_separator = * mail_prefetch_count = 30 passdb { driver = passwd-file - args = /etc/dovecot/dovecot-master.passwd + args = /usr/local/etc/dovecot/dovecot-master.passwd master = yes pass = yes } passdb { - args = /etc/dovecot/sql/dovecot-mysql.conf + args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } namespace inbox { @@ -202,15 +201,15 @@ listen = *,[::] ssl_cert = +
+
+
+ +
+
+
diff --git a/data/web/admin.php b/data/web/admin.php index 5377616f..e0e04aee 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -81,14 +81,14 @@ $tfa_data = get_tfa();
- +
- - - - - + + + + + @@ -299,8 +299,14 @@ $tfa_data = get_tfa(); - - + +tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} \ No newline at end of file diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index b5c69343..23f5ad84 100644 --- a/data/web/css/mailbox.css +++ b/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; } \ No newline at end of file diff --git a/data/web/css/tables.css b/data/web/css/tables.css deleted file mode 100644 index 651e1665..00000000 --- a/data/web/css/tables.css +++ /dev/null @@ -1,79 +0,0 @@ -ul[id*="sortable"] { word-wrap: break-word; list-style-type: none; float: left; padding: 0 15px 0 0; width: 48%; cursor:move} -ul[id$="sortable-active"] li {cursor:move; } -ul[id$="sortable-inactive"] li {cursor:move } -.list-heading { cursor:default !important} -.ui-state-disabled { cursor:no-drop; color:#ccc; } -.ui-state-highlight {background: #F5F5F5 !important; height: 41px !important; cursor:move } -table[data-sortable] { - border-collapse: collapse; - border-spacing: 0; -} -table[data-sortable] th { - vertical-align: bottom; - font-weight: bold; -} -table[data-sortable] th, table[data-sortable] td { - text-align: left; - padding: 10px; -} -table[data-sortable] th:not([data-sortable="false"]) { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-touch-callout: none; - cursor: pointer; -} -table[data-sortable] th:after { - content: ""; - visibility: hidden; - display: inline-block; - vertical-align: inherit; - height: 0; - width: 0; - border-width: 5px; - border-style: solid; - border-color: transparent; - margin-right: 1px; - margin-left: 10px; - float: right; -} -table[data-sortable] th[data-sortable="false"]:after { - display: none; -} -table[data-sortable] th[data-sorted="true"]:after { - visibility: visible; -} -table[data-sortable] th[data-sorted-direction="descending"]:after { - border-top-color: inherit; - margin-top: 8px; -} -table[data-sortable] th[data-sorted-direction="ascending"]:after { - border-bottom-color: inherit; - margin-top: 3px; -} -table[data-sortable].sortable-theme-bootstrap thead th { - border-bottom: 2px solid #e0e0e0; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"] { - color: #3a87ad; - background: #d9edf7; - border-bottom-color: #bce8f1; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="descending"]:after { - border-top-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="ascending"]:after { - border-bottom-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap.sortable-theme-bootstrap-striped tbody > tr:nth-child(odd) > td { - background-color: #f9f9f9; -} -#data td, #no-data td { - vertical-align: middle; -} -.sort-table:hover { - border-bottom-color: #00B7DC !important; -} \ No newline at end of file diff --git a/data/web/edit.php b/data/web/edit.php index 5879df6d..3f3311d0 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -620,6 +620,20 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == +
+
+
+ +
+
+
+
+
+
+ +
+
+
diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 5523ec58..580c457f 100644 --- a/data/web/inc/footer.inc.php +++ b/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':'', - }, + url: "/api/v1/u2f-authentication/", 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':'', - }, + url: "/api/v1/u2f-registration/", success: function(data){ data; } diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index c37e39ab..465ab2ac 100644 --- a/data/web/inc/functions.inc.php +++ b/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( diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a206a35d..ede05bed 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -16,11 +16,10 @@ - - ' : null;?> +' : null;?> diff --git a/data/web/js/add.js b/data/web/js/add.js index 1ebcdbb2..54a70500 100644 --- a/data/web/js/add.js +++ b/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') { diff --git a/data/web/js/admin.js b/data/web/js/admin.js index a235a422..feaf3907 100644 --- a/data/web/js/admin.js +++ b/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 = $("").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 = $("").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 = ''; + }); + $('#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 + } + }); + } + }); }); \ No newline at end of file diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 1ddf0c35..4e6b303d 100644 --- a/data/web/js/mailbox.js +++ b/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'); diff --git a/data/web/js/sorttable.js b/data/web/js/sorttable.js deleted file mode 100644 index cb3e293c..00000000 --- a/data/web/js/sorttable.js +++ /dev/null @@ -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); diff --git a/data/web/json_api.php b/data/web/json_api.php index f0715466..66b46cfb 100644 --- a/data/web/json_api.php +++ b/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); + 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); } } - } - 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; } - 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); + 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); } } - } - 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; + } - 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); + 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); } } - } - 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; + } - 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); + 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); } } - } - 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; } - else { - echo '{}'; + 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 "get_mailbox_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_mailbox_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; + 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; } - 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); - } - 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; } } } \ No newline at end of file diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index eef252e9..07d23d70 100644 --- a/data/web/lang/lang.de.php +++ b/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'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.'; +$lang['user']['tls_policy_warning'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.
Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox direkt zugeordnet 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'; ?> diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index a9f2f51c..978ee713 100644 --- a/data/web/lang/lang.en.php +++ b/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'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.'; +$lang['user']['tls_policy_warning'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.
This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses with only this single mailbox 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'; ?> diff --git a/data/web/lang/lang.ru.php b/data/web/lang/lang.ru.php index e2502b83..d07bbc9f 100644 --- a/data/web/lang/lang.ru.php +++ b/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'] = "Активный"; diff --git a/data/web/mailbox.php b/data/web/mailbox.php index bc8ca6d2..fe2cd803 100644 --- a/data/web/mailbox.php +++ b/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']; ?> -
-

tbody>tr.footable-empty>td { endif; ?>
+

@@ -49,14 +28,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -65,14 +45,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -80,14 +61,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -100,10 +82,10 @@ table.footable>tbody>tr.footable-empty>td {
-

+

diff --git a/data/web/user.php b/data/web/user.php index 94f2a762..03e1a22b 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -405,7 +405,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
- +
@@ -416,7 +416,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' - + diff --git a/docker-compose.yml b/docker-compose.yml index 76ca1b6b..b3d18790 100644 --- a/docker-compose.yml +++ b/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: diff --git a/docs/first_steps.md b/docs/first_steps.md index 7f835bc4..ab7876dc 100644 --- a/docs/first_steps.md +++ b/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. diff --git a/docs/install.md b/docs/install.md index 7c383a63..b9322f00 100644 --- a/docs/install.md +++ b/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 diff --git a/docs/u_and_e.md b/docs/u_and_e.md index d0a52cc8..3f52b05e 100644 --- a/docs/u_and_e.md +++ b/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.
Server:Port Log