Skip to content

Commit

Permalink
Release v1.1
Browse files Browse the repository at this point in the history
- Optimization in Docker Image Size
- Configuration by ENV-variables
- SMTP Fallback for incompatible E-Mails
- Cleaned Container Log
- Bug Fixes
  • Loading branch information
JM-Lemmi authored Jan 2, 2022
2 parents f38e213 + 587cac3 commit e8b3d8d
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 121 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,4 @@ MigrationBackup/
mount
.devcontainer
env
/docker-testing
61 changes: 21 additions & 40 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,70 +1,51 @@
FROM alpine:latest

# Install dependencies
#RUN apk add --no-cache --update postfix ca-certificates socat acme.sh bash && \
RUN apk add --no-cache --update postfix dovecot ca-certificates git gcc musl-dev linux-headers libmilter-dev

#install python
RUN apk add --update --no-cache python3 python3-dev && ln -sf python3 /usr/bin/python
RUN python3 -m ensurepip
RUN pip3 install --no-cache --upgrade pip setuptools
RUN python3 -m ensurepip; pip3 install --no-cache --upgrade pip setuptools

RUN pip3 install zimbraweb git+https://github.com/sdgathman/pymilter

RUN pip3 install zimbraweb
#postfix basic config
RUN postconf -e "mynetworks=0.0.0.0/0" "maillog_file=/var/log/log" "smtpd_sasl_path=private/auth" "smtpd_sasl_type=dovecot" "smtpd_sasl_auth_enable=yes" "smtpd_delay_reject=yes" "smtpd_client_restrictions=permit_sasl_authenticated,reject" "smtpd_milters=unix:/milter.sock"

RUN pip3 install git+https://github.com/sdgathman/pymilter

#postfix config
RUN postconf -e mynetworks=0.0.0.0/0
RUN postconf -e "maillog_file=/dev/stdout"
RUN postconf -e smtpd_sasl_path=private/auth
RUN postconf -e smtpd_sasl_type=dovecot
RUN postconf -e smtpd_sasl_auth_enable=yes
RUN postconf -e smtpd_delay_reject=yes
RUN postconf -e smtpd_client_restrictions=permit_sasl_authenticated,reject
RUN postconf -e smtpd_milters=unix:/milter.sock

#add script execution
#https://contrid.net/server/mail-servers/postfix-catch-all-pipe-to-script
RUN touch /etc/postfix/virtual_aliases
#postfix transport script execution
RUN adduser --disabled-password posttransport
RUN touch /var/log/log
RUN chmod -R 777 /var/log/log
RUN echo "* zimbrawebtransport:" > /etc/postfix/transport
#not needed when texthash RUN postmap /etc/postfix/virtual_aliases
#not needed when texthash RUN postmap /etc/postfix/transport
#zusammen mit -e muss bei echo $ escaped werden
RUN echo -e "zimbrawebtransport unix - n n - - pipe\n flags=FR user=nobody argv=/srv/zimbraweb/send_mail.py\n \${nexthop} \${user} \${sasl_username}" >> /etc/postfix/master.cf
RUN echo -e "transport_maps = texthash:/etc/postfix/transport\nvirtual_alias_maps = texthash:/etc/postfix/virtual_aliases" >> /etc/postfix/main.cf
RUN echo -e "zimbrawebtransport unix - n n - - pipe\n flags=FR user=posttransport argv=/srv/zimbraweb/send_mail.py\n \${nexthop} \${user} \${sasl_username}" >> /etc/postfix/master.cf
RUN postconf -e "transport_maps=texthash:/etc/postfix/transport"

RUN echo -e "submission inet n - y - - smtpd" >> /etc/postfix/master.cf
RUN echo -e " -o syslog_name=postfix/submission" >> /etc/postfix/master.cf
RUN echo -e " -o smtpd_sasl_auth_enable=yes" >> /etc/postfix/master.cf
RUN echo -e " -o smtpd_sasl_path=private/auth" >> /etc/postfix/master.cf
RUN echo -e " -o smtpd_client_restrictions=permit_sasl_authenticated,reject" >> /etc/postfix/master.cf
RUN echo -e "submission inet n - y - - smtpd\n -o syslog_name=postfix/submission\n -o smtpd_sasl_auth_enable=yes\n -o smtpd_sasl_path=private/auth\n -o smtpd_client_restrictions=permit_sasl_authenticated,reject" >> /etc/postfix/master.cf

#dovecot config
ADD ./files/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf
ADD ./files/dovecot/conf.d/10-master.conf /etc/dovecot/conf.d/10-master.conf
ADD ./files/dovecot/conf.d/auth-checkpassword.conf.ext /etc/dovecot/conf.d/auth-checkpassword.conf.ext
ADD ./files/dovecot/conf.d/ /etc/dovecot/conf.d/

#copy python scripts
ADD ./files/*.py /srv/zimbraweb/
RUN chmod 777 /srv/zimbraweb/*.py

RUN mkdir /srv/zimbraweb/mnt/
RUN chmod -R 777 /srv/zimbraweb/mnt/

RUN mkdir /srv/zimbraweb/logs/
RUN chmod -R 777 /srv/zimbraweb/logs/

#config mount
RUN mkdir /srv/zimbraweb/mnt/; chmod -R 777 /srv/zimbraweb/mnt/
VOLUME /srv/zimbraweb/mnt/

# Add crontab to delete expired auth tokens
RUN crontab -l /cron
RUN echo "* * * * * find /dev/shm/ -name auth_* -type f -perm 444 -mmin +3 -delete" >> /cron
RUN crontab /cron
RUN rm /cron

# Expose smtp submission port
EXPOSE 587

ADD ./files/start.sh /
RUN chmod +x /start.sh

ADD ./files/tls.sh /
RUN chmod +x /tls.sh
RUN mkdir /tls/
RUN chmod +x /tls.sh; mkdir /tls/

CMD ["/start.sh"]
54 changes: 48 additions & 6 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This Container allows users to send E-Mails via SMTP to a Zimbra Web Interface.

## Public Bridge

There is a public server available at dhbw-mannheim.email at port 2525. Connect to it via SMTP with STARTTLS. For increased security we recommend hosting the Bridge yourself if you have a server available, [more on that below](#self-hosting). The public bridge is configured to automatically purge all data every 60 minutes. No logging data is written to disk at all, auth tokens (which are needed to authenticated with the Web Client) are kept only in memory and for 60 minutes at most, but in almost all cases will be deleted immediately after successful email delivery.
There is a public server available at dhbw-mannheim.email at port 2525. Connect to it via SMTP with STARTTLS. For increased security we recommend hosting the Bridge yourself if you have a server available, [more on that below](#self-hosting). No logging data is written to disk at all, auth tokens (which are needed to authenticated with the Web Client) are kept only in memory and for 3 minutes at most, but in almost all cases will be deleted immediately after successful email delivery.

You can use the following settings:

Expand Down Expand Up @@ -39,11 +39,42 @@ In Thunderbird you should go to Acccount Settings, select "Composition & Address
To start the container use the following command

```
docker run --name smtp_bridge -h <your_domain_name> -p 587:587 ghcr.io/cirosec-studis/zimbraweb-smtp-bridge:nightly
docker run --name smtp_bridge -p 587:587 ghcr.io/cirosec-studis/zimbraweb-smtp-bridge:nightly
```

If you do not have a domain name associated with the server, you can use whatever hostname you want, e.g. "smtp_bridge.local".

### Configuration

There are two options for configuring. A default configuration will be generated for all values that are not set manually.

The default configuration is as follows:

```json
{
"zimbra_host": "https://studgate.dhbw-mannheim.de",
"email_domain": "student.dhbw-mannheim.de",
"smtp_fallback": "disabled",
"smtp_fallback_relay_host": "172.17.0.2",
"log_level": "info",
}
```

#### permitted configuration values

smtp_fallback: enabled, disabled
log_level: *debug* - DEBUG, any other value - INFO

#### config.json

Put a `config.json` file into the mounted folder at `/srv/zimbraweb/mnt/`.

#### ENV variables

In docker ENV variables can be set with the `-e` flag.

Set `ENVCONFIG=true` to enable configuration via ENV Variables. Then set all other ENV-variables to the configuration value you want.

### TLS Support

TLS is enabled by default, using a self-signed certificate for the hostname you provided. This will be enough in most cases, you will just need to accept the self-signed certificate in your email client. Thunderbird and Outlook will tell you that the certificate could not be verified. You will need to add an exception.
Expand Down Expand Up @@ -75,12 +106,8 @@ The following tags are available:
* `ghcr.io/cirosec-studis/zimbraweb-smtp-bridge:vX.Y.Z` - Version X.Y.Z (e.g. v1.0.0)
* `ghcr.io/cirosec-studis/zimbraweb-smtp-bridge:develop` - development build



If you're on a raspberry pi, note the section [Docker on Raspberry Pi](#docker-on-raspberry-pi).

Optionally mount a logs directory by adding `-v /path/to/logs:/srv/zimbraweb/logs/`.

You can now connect to the container with your SMTP client at localhost:587.
To authenticate, use your Zimbra Webclient login credentials (without the @domain.tld part!).

Expand All @@ -100,3 +127,18 @@ rpi ~$ sudo apt install libseccomp2 -t buster-backports
```

This fix is from https://blog.samcater.com/fix-workaround-rpi4-docker-libseccomp2-docker-20/. Alpine requires libseccomp2>2.4.2 but on debian buster has 2.3.6, this fix installes a newer version from the backports repository.

### SMTP Relay function

Zimbra has certain limitations, like not supporting html Emails. If you want to route these unsupported E-Mails via another SMTP relay, use this function. It is disabled by default.

❗ be careful with this option. It can lead to your Mail being classified as Spam or outright rejected by the receiving Server due to wrong Origin.

If you want to take the easy option use the `docker-compose.yml` provided in this repository.

Set the `smtp_fallback` configuration option to `"enabled"` and `smtp_fallback_relay_host` to a Mailserver that accepts Mail on Port 25.

### Known Limitations

* Currently authentication to the Relay host is not supported.
* Naming the Container after the Mail-Domain of the Zimbra Server or any other Domain, that may be the recipient of actual Mail is not supported and will lead to errors in delivering mail. (See #34)
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This Docker compose is configured with SMTP-Relay. Please consult the Readme if you want this option. Otherwise plase use the plain container and not this compose file.

version: '3'
services:
zimbraweb:
image: 'ghcr.io/cirosec-studis/zimbraweb-smtp-bridge:latest'
restart: unless-stopped
ports:
- '587:587'
environment:
- ENVCONFIG=true
- smtp_fallback=enabled
- smtp_fallback_relay_host=relayhost

relayhost:
image: boky/postfix
restart: unless-stopped
environment:
- ALLOWED_SENDER_DOMAINS=student.dhbw-mannheim.de
8 changes: 0 additions & 8 deletions docker-testing/Dockerfile

This file was deleted.

5 changes: 0 additions & 5 deletions docker-testing/test.py

This file was deleted.

7 changes: 7 additions & 0 deletions files/hostnamefilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import logging, platform

class HostnameFilter(logging.Filter):
hostname = platform.node()
def filter(self, record):
record.hostname = HostnameFilter.hostname
return True
32 changes: 24 additions & 8 deletions files/send_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
import pickle
import logging
from email.parser import Parser
import smtplib

from zimbraweb import WebkitAttachment, ZimbraUser
from zimbraweb import WebkitAttachment, ZimbraUser, emlparsing
from zimbra_config import get_config

CONFIG = get_config()

file_handler = logging.FileHandler(
filename='/srv/zimbraweb/logs/send_mail.log')
stdout_handler = logging.StreamHandler(sys.stdout)
handlers = [file_handler, stdout_handler]

logging.basicConfig(handlers=handlers, level=logging.INFO)
#setting up logger
import hostnamefilter
file_handler = logging.FileHandler(filename='/var/log/log')
#stream_handler = logging.StreamHandler()
file_handler.addFilter(hostnamefilter.HostnameFilter())
file_handler.setFormatter(logging.Formatter('%(asctime)s %(hostname)s python/%(filename)s: %(message)s', datefmt='%b %d %H:%M:%S'))
handlers = [file_handler]
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
if (CONFIG['log_level'] == "debug"): logging.basicConfig(handlers=handlers, level=logging.DEBUG)
else: logging.basicConfig(handlers=handlers, level=logging.INFO)

logging.info("send_mail started!")

Expand Down Expand Up @@ -53,7 +59,8 @@ def send_as_user(user, payload, boundary):
return result


if ZIMBRA_USERNAME.strip() == "": # this is a bounce email
# this is a bounce email
if ZIMBRA_USERNAME.strip() == "":
RECEIVER = sys.argv[2]
if not os.path.isfile(f"/dev/shm/auth_{RECEIVER}"):
logging.error(
Expand All @@ -72,6 +79,7 @@ def send_as_user(user, payload, boundary):
result = send_as_user(user, payload, boundary)
logging.info(f"Bounce sent: {result=}")
exit(0)

# special case: Outlook test email.
elif parsed["From"] == f'"Microsoft Outlook" <{ZIMBRA_USERNAME}@{CONFIG["email_domain"]}>':
logging.info("Sending outlook test email via text/plain")
Expand All @@ -80,6 +88,14 @@ def send_as_user(user, payload, boundary):
body="Diese E-Mail-Nachricht wurde von Microsoft Outlook automatisch während des Testens der Kontoeinstellungen gesendet.")
logging.info(f"Outlook test email sent: {result=}")
exit(0 if result.success else 1)

# html mail via smtp relay, if smtpfallback is enabled
elif not emlparsing.is_parsable(raw_eml) and CONFIG['smtp_fallback'] == "enabled":
logging.info("Mail not parsable by Zimbra. Using SMTP relay instead.")
with smtplib.SMTP(CONFIG['smtp_fallback_relay_host']) as s:
s.send_message(parsed)

# default: send mail via Zimbra
else:
if f"{ZIMBRA_USERNAME}@{CONFIG['email_domain']}" not in parsed.get("From"):
logging.error(
Expand Down
8 changes: 7 additions & 1 deletion files/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
python3 /srv/zimbraweb/zimbra_config.py
/tls.sh
dovecot
echo "$(date +"%b %d %H:%M:%S") $HOSTNAME start.sh[$$]: Started dovecot."
postfix start
python3 /srv/zimbraweb/zimbra_milter.py
crond
python3 /srv/zimbraweb/zimbra_milter.py &
sleep 2
echo "$(date +"%b %d %H:%M:%S") $HOSTNAME start.sh[$$]: ➔ Switching to log output from '/var/log/log'"
echo "$(date +"%b %d %H:%M:%S") $HOSTNAME start.sh[$$]: System is now operational and ready to receive E-Mails" >> /var/log/log
tail -f /var/log/log
8 changes: 4 additions & 4 deletions files/tls.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ keyfile=/tls/key.pem

if [ ! -f "$certfile" ]; then
# generate a self signed certificate (valid for 10 years)
openssl req -x509 -newkey rsa:4096 -keyout $keyfile -out $certfile -sha256 -days 3650 -nodes -subj "/CN=$HOSTNAME"
echo "$(date +"%b %d %H:%M:%S") $HOSTNAME start.sh/tls.sh[$$]: No certfile found, generating new self-signed cert..."
openssl req -x509 -newkey rsa:4096 -keyout $keyfile -out $certfile -sha256 -days 3650 -nodes -subj "/CN=$HOSTNAME" &>/dev/null
echo "$(date +"%b %d %H:%M:%S") $HOSTNAME start.sh/tls.sh[$$]: Writing new private key to '$certfile'"
fi

chmod 600 $certfile
chmod 600 $keyfile

echo "$(date +"%b %d %H:%M:%S") $HOSTNAME start.sh/tls.sh[$$]: Writing Postfix conf for TLS"
postconf -e myhostname=$HOSTNAME
postconf -e "smtpd_tls_cert_file = ${certfile}"
postconf -e "smtpd_tls_key_file = ${keyfile}"
Expand All @@ -19,6 +22,3 @@ postconf -e 'smtpd_tls_security_level = may'
postconf -e 'smtp_tls_note_starttls_offer = yes'
postconf -e 'smtpd_tls_loglevel = 1'
postconf -e 'smtpd_tls_received_header = yes'

postfix stop
postfix start
13 changes: 12 additions & 1 deletion files/zimbra_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@
import sys
import pickle
import logging
import sys

from zimbraweb import ZimbraUser
from zimbra_config import get_config

CONFIG = get_config()

logging.basicConfig(filename='/srv/zimbraweb/logs/authentication.log', level=logging.INFO)
#setting up logger
import hostnamefilter
handler = logging.FileHandler(filename='/var/log/log')
#handler = logging.StreamHandler(sys.stdout)
handler.addFilter(hostnamefilter.HostnameFilter())
handler.setFormatter(logging.Formatter('%(asctime)s %(hostname)s python/%(filename)s: %(message)s', datefmt='%b %d %H:%M:%S'))
handlers = [handler]
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
if (CONFIG['log_level'] == "debug"): logging.basicConfig(handlers=handlers, level=logging.DEBUG)
else: logging.basicConfig(handlers=handlers, level=logging.INFO)

data = os.read(3, 1024).split(b"\x00")
AUTH_USERNAME = data[0].decode("utf8")
Expand Down
Loading

0 comments on commit e8b3d8d

Please sign in to comment.