public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
Base on Stretch, add Zeyple
2b955d08
data/Dockerfiles/postfix/Dockerfile | 15 +-
data/Dockerfiles/postfix/postfix.sh | 14 +-
data/Dockerfiles/postfix/supervisord.conf | 15 +-
data/Dockerfiles/postfix/zeyple.conf | 9 +
data/Dockerfiles/postfix/zeyple.py | 274 ++++++++++++++++++++++++++++++
5 files changed, 320 insertions(+), 7 deletions(-)
create mode 100644 data/Dockerfiles/postfix/zeyple.conf
create mode 100755 data/Dockerfiles/postfix/zeyple.py
Diff
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 <[email protected]>
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..f2ba03e4 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 <<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,21 @@ 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
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)