Merge remote-tracking branch 'upstream/2024.06' into oidc

This commit is contained in:
Sebastian Wilke
2024-12-17 20:07:41 +01:00
63 changed files with 402 additions and 662 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ pip-selfcheck.json
/docs/lib*
/docs/bin
/docs/include
/docs/contributors/mailu-network-diagram.svg
/docs/_build
/.env
/.venv

View File

@@ -23,7 +23,7 @@ RUN set -euxo pipefail \
RUN echo $VERSION >/version
#EXPOSE 8080/tcp
HEALTHCHECK CMD curl -skfLo /dev/null http://localhost:8080/ping
HEALTHCHECK CMD curl -m3 -skfLo /dev/null http://localhost:8080/ping
VOLUME ["/data","/dkim"]

View File

@@ -21,6 +21,9 @@ function sha1(string) {
}
function hibpCheck(pwd) {
if (pwd === null || pwd === undefined || pwd.length === 0) {
return;
}
// We hash the pwd first
sha1(pwd).then(function(hash){
// We send the first 5 chars of the hash to hibp's API

View File

@@ -18,7 +18,11 @@ STATUSES = {
"sieve": "AuthFailed"
}),
"encryption": ("Must issue a STARTTLS command first", {
"smtp": "530 5.7.0"
"imap": "PRIVACYREQUIRED",
"smtp": "530 5.7.0",
"submission": "530 5.7.0",
"pop3": "-ERR Authentication canceled.",
"sieve": "ENCRYPT-NEEDED"
}),
"ratelimit": ("Temporary authentication failure (rate-limit)", {
"imap": "LIMIT",
@@ -68,7 +72,7 @@ def handle_authentication(headers):
# Incoming mail, no authentication
if method in ['', 'none'] and protocol in ['smtp', 'lmtp']:
server, port = get_server(protocol, False)
if app.config["INBOUND_TLS_ENFORCE"]:
if app.config["INBOUND_TLS_ENFORCE"] and protocol == 'smtp':
if "Auth-SSL" in headers and headers["Auth-SSL"] == "on":
return {
"Auth-Status": "OK",
@@ -91,20 +95,14 @@ def handle_authentication(headers):
# Authenticated user
elif method in ['plain', 'login']:
is_valid_user = False
# According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should
# be ASCII and are generally considered ISO8859-1. However when passing
# the password, nginx does not transcode the input UTF string, thus
# we need to manually decode.
raw_user_email = urllib.parse.unquote(headers["Auth-User"])
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
user_email = 'invalid'
password = 'invalid'
try:
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
password = raw_password.encode("iso8859-1").decode("utf8")
user_email = urllib.parse.unquote(headers["Auth-User"])
password = urllib.parse.unquote(headers["Auth-Pass"])
ip = urllib.parse.unquote(headers["Client-Ip"])
except:
app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
app.logger.warn(f'Received undecodable user/password from front: {headers.get("Auth-User", "")!r}')
else:
try:
user = models.User.query.get(user_email) if '@' in user_email else None

View File

@@ -29,7 +29,6 @@ def nginx_authentication():
response.headers['Auth-Status'] = status
response.headers['Auth-Error-Code'] = code
return response
raw_password = urllib.parse.unquote(headers['Auth-Pass']) if 'Auth-Pass' in headers else ''
headers = nginx.handle_authentication(flask.request.headers)
response = flask.Response()
for key, value in headers.items():
@@ -50,14 +49,8 @@ def nginx_authentication():
if not is_port_25:
utils.limiter.exempt_ip_from_ratelimits(client_ip)
elif is_valid_user:
password = None
try:
password = raw_password.encode("iso8859-1").decode("utf8")
except:
app.logger.warn(f'Received undecodable password for {username} from nginx: {raw_password!r}')
utils.limiter.rate_limit_user(username, client_ip, password=None)
else:
utils.limiter.rate_limit_user(username, client_ip, password=password)
password = urllib.parse.unquote(headers.get('Auth-Pass', ''))
utils.limiter.rate_limit_user(username, client_ip, password=password)
elif not is_from_webmail:
utils.limiter.rate_limit_ip(client_ip, username)
return response

View File

@@ -279,7 +279,7 @@ class Domain(Base):
f'_{proto}._tcp.{self.name}. 600 IN SRV {prio} 1 {port} {hostname}.' if port in ports else f'_{proto}._tcp.{self.name}. 600 IN SRV 0 0 0 .'
for proto, port, prio
in protocols
]+[f'autoconfig.{self.name}. 600 IN CNAME {hostname}.']
]+[f'autoconfig.{self.name}. 600 IN CNAME {hostname}.', f'autodiscover.{self.name}. 600 IN CNAME {hostname}.']
@cached_property
def dns_tlsa(self):
@@ -680,7 +680,7 @@ in clear-text regardless of the presence of the cache.
set() containing the sessions to keep
"""
self.password = password if raw else User.get_password_context().hash(password)
if keep_sessions is not True:
if keep_sessions is not True and self.email is not None:
utils.MailuSessionExtension.prune_sessions(uid=self.email, keep=keep_sessions)
def get_managed_domains(self):

View File

@@ -24,7 +24,7 @@ def user_create(domain_name):
flask.url_for('.user_list', domain_name=domain.name))
form = forms.UserForm()
form.pw.validators = [wtforms.validators.DataRequired()]
form.quota_bytes.default = app.config['DEFAULT_QUOTA']
form.quota_bytes.default = int(app.config['DEFAULT_QUOTA'])
if domain.max_quota_bytes:
form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
@@ -93,12 +93,12 @@ def user_settings(user_email):
form = forms.UserSettingsForm(obj=user)
utils.formatCSVField(form.forward_destination)
if form.validate_on_submit():
if form.forward_enabled.data and (form.forward_destination.data in ['', None] or type(form.forward_destination.data) is list):
user.forward_enabled = bool(flask.request.form.get('forward_enabled', False))
if user.forward_enabled and not form.forward_destination.data:
flask.flash('Destination email address is missing', 'error')
user.forward_enabled = True
return flask.render_template('user/settings.html', form=form, user=user)
if form.forward_enabled.data:
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
return flask.redirect(
flask.url_for('.user_settings', user_email=user_email))
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
form.populate_obj(user)
models.db.session.commit()
form.forward_destination.data = ", ".join(form.forward_destination.data)
@@ -107,8 +107,9 @@ def user_settings(user_email):
return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name))
elif form.is_submitted() and not form.validate():
user.forward_enabled = form.forward_enabled.data
return flask.render_template('user/settings.html', form=form, user=user)
flask.flash('Error validating the form', 'error')
return flask.redirect(
flask.url_for('.user_settings', user_email=user_email))
return flask.render_template('user/settings.html', form=form, user=user)
def _process_password_change(form, user_email):

View File

@@ -698,6 +698,7 @@ def isBadOrPwned(form):
def formatCSVField(field):
if not field.data:
field.data = ''
return
if isinstance(field.data,str):
data = field.data.replace(" ","").split(",")

View File

@@ -64,7 +64,7 @@ test_unsupported()
cmdline = [
"gunicorn",
"--threads", f"{os.cpu_count()}",
"--threads", os.environ.get('CPU_COUNT', '1'),
# If SUBNET6 is defined, gunicorn must listen on IPv6 as well as IPv4
"-b", f"{'[::]' if os.environ.get('SUBNET6') else '0.0.0.0'}:8080",
"--logger-class mailu.Logger",

View File

@@ -3,7 +3,7 @@
# base system image (intermediate)
# Note when updating the alpine tag, first manually run the workflow .github/workflows/mirror.yml.
# Just run the workflow with the tag that must be synchronised.
ARG DISTRO=ghcr.io/mailu/alpine:3.20
ARG DISTRO=ghcr.io/mailu/alpine:3.20.3
FROM $DISTRO as system
ENV TZ=Etc/UTC LANG=C.UTF-8

View File

@@ -31,30 +31,29 @@ def _coerce_value(value):
class LogFilter(object):
def __init__(self, stream, re_patterns):
self.stream = stream
if isinstance(re_patterns, list):
self.pattern = re.compile('|'.join([fr'(?:{pattern})' for pattern in re_patterns]))
elif isinstance(re_patterns, str):
self.pattern = re.compile(re_patterns)
else:
self.pattern = re_patterns
self.found = False
self.stream = stream
self.pattern = re.compile(b'|'.join([b''.join([b'(?:', pattern, b')']) for pattern in re_patterns]))
self.buffer = b''
def __getattr__(self, attr_name):
return getattr(self.stream, attr_name)
def write(self, data):
if data == '\n' and self.found:
self.found = False
else:
if not self.pattern.search(data):
self.stream.write(data)
if type(data) is str:
data = data.encode('utf-8')
self.buffer += data
while b'\n' in self.buffer:
line, cr, rest = self.buffer.partition(b'\n')
if not self.pattern.search(line):
self.stream.buffer.write(line)
self.stream.buffer.write(cr)
self.stream.flush()
else:
# caught bad pattern
self.found = True
self.buffer = rest
def flush(self):
# write out buffer on flush even if it's not a complete line
if self.buffer and not self.pattern.search(self.buffer):
self.stream.buffer.write(self.buffer)
self.stream.flush()
def _is_compatible_with_hardened_malloc():
@@ -100,7 +99,7 @@ def set_env(required_secrets=[], log_filters=[]):
for secret in required_secrets:
os.environ[f'{secret}_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray(secret, 'utf-8'), 'sha256').hexdigest()
os.system('find /run -xdev -type f -name \*.pid -print -delete')
os.system(r'find /run -xdev -type f -name \*.pid -print -delete')
return {
key: _coerce_value(os.environ.get(key, value))
@@ -108,8 +107,41 @@ def set_env(required_secrets=[], log_filters=[]):
}
def clean_env():
""" remove all secret keys """
""" remove all secret keys, normalize PROXY_PROTOCOL """
[os.environ.pop(key, None) for key in os.environ.keys() if key.endswith("_KEY")]
# Configure PROXY_PROTOCOL
PROTO_MAIL=['25', '110', '995', '143', '993', '587', '465', '4190']
PROTO_ALL_BUT_HTTP=PROTO_MAIL.copy()
PROTO_ALL_BUT_HTTP.extend(['443'])
PROTO_ALL=PROTO_ALL_BUT_HTTP.copy()
PROTO_ALL.extend(['80'])
for item in os.environ.get('PROXY_PROTOCOL', '').split(','):
if item.isdigit():
os.environ[f'PROXY_PROTOCOL_{item}']='True'
elif item == 'mail':
for p in PROTO_MAIL: os.environ[f'PROXY_PROTOCOL_{p}']='True'
elif item == 'all-but-http':
for p in PROTO_ALL_BUT_HTTP: os.environ[f'PROXY_PROTOCOL_{p}']='True'
elif item == 'all':
for p in PROTO_ALL: os.environ[f'PROXY_PROTOCOL_{p}']='True'
elif item == '':
pass
else:
log.error(f'Not sure what to do with {item} in PROXY_PROTOCOL ({args.get("PROXY_PROTOCOL")})')
PORTS_REQUIRING_TLS=['443', '465', '993', '995']
ALL_PORTS='25,80,443,465,993,995,4190'
for item in os.environ.get('PORTS', ALL_PORTS).split(','):
if item in PORTS_REQUIRING_TLS and os.environ.get('TLS_FLAVOR','') == 'notls':
continue
os.environ[f'PORT_{item}']='True'
if os.environ.get('TLS_FLAVOR', '') != 'notls':
for item in os.environ.get('TLS', ALL_PORTS).split(','):
if item in PORTS_REQUIRING_TLS:
os.environ[f'TLS_{item}']='True'
if 'CPU_COUNT' not in os.environ:
os.environ['CPU_COUNT'] = str(os.cpu_count())
def drop_privs_to(username='mailu'):
pwnam = getpwnam(username)
@@ -127,7 +159,7 @@ def forward_text_lines(src, dst):
# runs a process and passes its standard/error output to the standard/error output of the current python script
def run_process_and_forward_output(cmd):
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_thread = threading.Thread(target=forward_text_lines, args=(process.stdout, sys.stdout))
stdout_thread.daemon = True
@@ -137,4 +169,7 @@ def run_process_and_forward_output(cmd):
stderr_thread.daemon = True
stderr_thread.start()
process.wait()
rc = process.wait()
sys.stdout.flush()
sys.stderr.flush()
return rc

View File

@@ -137,12 +137,22 @@ service imap-login {
inet_listener imap {
port = 143
}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
service pop3-login {
inet_listener pop3 {
port = 110
}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
###############
@@ -166,6 +176,11 @@ service managesieve-login {
inet_listener sieve {
port = 4190
}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
protocol sieve {

View File

@@ -7,7 +7,9 @@ import multiprocessing
from podop import run_server
from socrate import system, conf
system.set_env(log_filters=[r'Error\: SSL context initialization failed, disabling SSL\: Can\'t load SSL certificate \(ssl_cert setting\)\: The certificate is empty$'])
system.set_env(log_filters=[
rb'Error\: SSL context initialization failed, disabling SSL\: Can\'t load SSL certificate \(ssl_cert setting\)\: The certificate is empty$'
])
def start_podop():
system.drop_privs_to('mail')

View File

@@ -29,7 +29,7 @@ RUN echo $VERSION >/version
EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 4190/tcp
# EXPOSE 10025/tcp 10143/tcp 14190/tcp
HEALTHCHECK --start-period=60s CMD curl -skfLo /dev/null http://127.0.0.1:10204/health && kill -0 `cat /run/dovecot/master.pid`
HEALTHCHECK --start-period=60s CMD curl -m3 -skfLo /dev/null http://127.0.0.1:10204/health && kill -0 `cat /run/dovecot/master.pid`
VOLUME ["/certs", "/overrides"]

View File

@@ -27,9 +27,9 @@ class ChangeHandler(FileSystemEventHandler):
if exists("/var/run/nginx.pid"):
print("Reloading a running nginx")
system("nginx -s reload")
if os.path.exists("/run/dovecot/master.pid"):
if exists("/run/dovecot/master.pid"):
print("Reloading a running dovecot")
os.system("doveadm reload")
system("doveadm reload")
@staticmethod
def reexec_config():

View File

@@ -66,6 +66,9 @@ http {
listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %};
{% endif %}
{% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %}
location ^~ /.well-known/acme-challenge/testing {
return 204;
}
location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008;
}
@@ -95,6 +98,7 @@ http {
set $webdav {{ WEBDAV_ADDRESS }}:5232;
{% endif %}
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
http2 on;
# Listen on HTTP only in kubernetes or behind reverse proxy
{% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %}
@@ -109,7 +113,6 @@ http {
listen 443 ssl{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %};
{% if SUBNET6 %}
listen [::]:443 ssl{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %};
http2 on;
{% endif %}
include /etc/nginx/tls.conf;
@@ -159,6 +162,9 @@ http {
}
{% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %}
location ^~ /.well-known/acme-challenge/testing {
return 204;
}
location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008;
}

View File

@@ -70,38 +70,6 @@ with open("/etc/resolv.conf") as handle:
resolver = content[content.index("nameserver") + 1]
args["RESOLVER"] = f"[{resolver}]" if ":" in resolver else resolver
# Configure PROXY_PROTOCOL
PROTO_MAIL=['25', '110', '995', '143', '993', '587', '465', '4190']
PROTO_ALL_BUT_HTTP=PROTO_MAIL.copy()
PROTO_ALL_BUT_HTTP.extend(['443'])
PROTO_ALL=PROTO_ALL_BUT_HTTP.copy()
PROTO_ALL.extend(['80'])
for item in args.get('PROXY_PROTOCOL', '').split(','):
if item.isdigit():
args[f'PROXY_PROTOCOL_{item}']=True
elif item == 'mail':
for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True
elif item == 'all-but-http':
for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True
elif item == 'all':
for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True
elif item == '':
pass
else:
log.error(f'Not sure what to do with {item} in PROXY_PROTOCOL ({args.get("PROXY_PROTOCOL")})')
PORTS_REQUIRING_TLS=['443', '465', '993', '995']
ALL_PORTS='25,80,443,465,993,995,4190'
for item in args.get('PORTS', ALL_PORTS).split(','):
if item in PORTS_REQUIRING_TLS and args['TLS_FLAVOR'] == 'notls':
continue
args[f'PORT_{item}']=True
if args['TLS_FLAVOR'] != 'notls':
for item in args.get('TLS', ALL_PORTS).split(','):
if item in PORTS_REQUIRING_TLS:
args[f'TLS_{item}']=True
# TLS configuration
cert_name = args.get("TLS_CERT_FILENAME", "cert.pem")
keypair_name = args.get("TLS_KEYPAIR_FILENAME", "key.pem")

View File

@@ -10,18 +10,28 @@ local http_client = dovecot.http.client {
max_attempts = 3;
}
-- on the other end we use urllib.parse.unquote()
function urlEncode(str)
return str:gsub("[^%w_.-~]", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
function auth_passdb_lookup(req)
local auth_request = http_client:request {
url = "http://{{ ADMIN_ADDRESS }}:8080/internal/auth/email";
}
auth_request:add_header('Auth-Port', req.local_port)
auth_request:add_header('Auth-User', req.user)
local user = urlEncode(req.user)
auth_request:add_header('Auth-User', user)
if req.password ~= nil
then
auth_request:add_header('Auth-Pass', req.password)
local password = urlEncode(req.password)
auth_request:add_header('Auth-Pass', password)
end
auth_request:add_header('Auth-Protocol', req.service)
auth_request:add_header('Client-IP', req.remote_ip)
local client_ip = urlEncode(req.remote_ip)
auth_request:add_header('Client-Ip', client_ip)
auth_request:add_header('Client-Port', req.remote_port)
auth_request:add_header('Auth-SSL', req.secured)
auth_request:add_header('Auth-Method', req.mechanism)

View File

@@ -87,6 +87,11 @@ service managesieve-login {
inet_listener sieve-webmail {
port = 14190
}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
{% endif %}
@@ -114,6 +119,11 @@ service imap-login {
inet_listener imap-webmail {
port = 10143
}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
service pop3-login {
@@ -132,6 +142,11 @@ service pop3-login {
{% endif %}
}
{% endif %}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
@@ -161,4 +176,11 @@ service submission-login {
inet_listener submission-webmail {
port = 10025
}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
}
!include_try /overrides/dovecot/proxy.conf

View File

@@ -6,8 +6,6 @@ import requests
import sys
import subprocess
import time
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
log.basicConfig(stream=sys.stderr, level="WARNING")
hostnames = ','.join(set(host.strip() for host in os.environ['HOSTNAMES'].split(',')))
@@ -22,6 +20,7 @@ command = [
"--preferred-challenges", "http", "--http-01-port", "8008",
"--keep-until-expiring",
"--allow-subset-of-names",
"--key-type", "rsa",
"--renew-with-new-domains",
"--config-dir", "/certs/letsencrypt",
"--post-hook", "/config.py"
@@ -45,33 +44,21 @@ command2 = [
# Wait for nginx to start
time.sleep(5)
class MyRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/.well-known/acme-challenge/testing':
self.send_response(204)
else:
self.send_response(404)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
def serve_one_request():
with HTTPServer(("127.0.0.1", 8008), MyRequestHandler) as server:
server.handle_request()
# Run certbot every day
while True:
while True:
hostname = os.environ['HOSTNAMES'].split(',')[0]
target = f'http://{hostname}/.well-known/acme-challenge/testing'
thread = Thread(target=serve_one_request)
thread.start()
r = requests.get(target)
if r.status_code != 204:
log.critical(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.")
time.sleep(5)
else:
break
thread.join()
try:
r = requests.get(target)
if r.status_code != 204:
log.critical(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.")
time.sleep(5)
else:
break
except Exception as e:
log.error(f"Exception while fetching {target}!", exc_info = e)
time.sleep(15)
subprocess.call(command)
subprocess.call(command2)

View File

@@ -4,7 +4,9 @@ import os
import subprocess
from socrate import system
system.set_env(log_filters=r'could not be resolved \(\d\: [^\)]+\) while in resolving client address, client\: [^,]+, server: [^\:]+\:(25|110|143|587|465|993|995)$')
system.set_env(log_filters=[
rb'could not be resolved \(\d\: [^\)]+\) while in resolving client address, client\: [^,]+, server: [^\:]+\:(25|110|143|587|465|993|995)$'
])
# Check if a stale pid file exists
if os.path.exists("/var/run/nginx.pid"):

View File

@@ -11,10 +11,10 @@ from podop import run_server
from socrate import system, conf
system.set_env(log_filters=[
r'(dis)?connect from localhost\[(\:\:1|127\.0\.0\.1)\]( quit=1 commands=1)?$',
r'haproxy read\: short protocol header\: QUIT$',
r'discarding EHLO keywords\: PIPELINING$'
])
rb'(dis)?connect from localhost\[(\:\:1|127\.0\.0\.1)\]( quit=1 commands=1)?$',
rb'haproxy read\: short protocol header\: QUIT$',
rb'discarding EHLO keywords\: PIPELINING$'
])
os.system("flock -n /queue/pid/master.pid rm /queue/pid/master.pid")

View File

@@ -17,7 +17,7 @@ COPY start.py /
RUN echo $VERSION >/version
#EXPOSE 11332/tcp 11334/tcp 11335/tcp
HEALTHCHECK --start-period=350s CMD curl -skfLo /dev/null http://localhost:11334/
HEALTHCHECK --start-period=350s CMD curl -m3 -skfLo /dev/null http://localhost:11334/
VOLUME ["/var/lib/rspamd"]

View File

@@ -9,7 +9,7 @@ COPY . /docs
RUN set -euxo pipefail \
; machine="$(uname -m)" \
; deps="gcc musl-dev" \
; deps="gcc musl-dev graphviz" \
; [[ "${machine}" != x86_64 ]] && \
deps="${deps} cargo" \
; apk add --no-cache --virtual .build-deps ${deps} \
@@ -17,7 +17,8 @@ RUN set -euxo pipefail \
mkdir -p /root/.cargo/registry/index && \
git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \
; pip3 install -r /requirements.txt \
; mkdir -p /build/$VERSION \
; mkdir -p /build/$VERSION/ \
; dot -Tsvg /docs/mailu-network-diagram.dot -o /docs/contributors/mailu-network-diagram.svg \
; sphinx-build -W /docs /build/$VERSION \
; apk del .build-deps \
; rm -rf /root/.cargo

View File

@@ -113,7 +113,7 @@ The following steps have to be taken to configure an additional symbol (rule) th
* soft reject: temporarily delay message (this is used, for instance, to greylist or rate-limit messages)
To move an email message to the Junk (Spam) folder, a score of 15 can be used in combination with the action "add header".
The above example configuration will reject all emails send from domains that are listed in '/etc/rspamd/override.d/blacklist.inc'.
The above example configuration will reject all emails send from domains that are listed in '/overrides/blacklist.inc'.
2. In the Rspamd overrides folder create a map that contains the domains to be blocked. You can use # to add comments.
@@ -137,12 +137,12 @@ The following steps have to be taken to configure an additional symbol (rule) th
The symbol is only displayed if the symbol has no pre-filter (action= line) configured. Changes made in this screen are not saved to the configuration file.
5. Check if the map is available. In rspamd webgui to to configuration. A map is available with the path:
/etc/rspamd/override.d/blacklist.inc Senders domain part is on the local blacklist
5. Check if the map is available. In rspamd webgui go to configuration, a map is available with the path:
/overrides/blacklist.inc Senders domain part is on the local blacklist
.. image:: assets/screenshots/RspamdMapBlacklist.png
When clicking on this map, you can live-edit the map via the GUI. Changes are effective immediately. Only changes made to maps in the overrides folder are persistent. Changes made to other maps will be reverted when the Rspamd container is recreated. It is also possible to make direct changes to the map on filesystem. These changes are also effective immediately.
When clicking on this map, you can live-edit the map via the GUI. Please note that only changes made to maps in the ``/overrides`` folder are persistent as changes made interractively though the GUI will be reverted when the Rspamd container is recreated. All changes (whether through the GUI or on the filesystem) are effective immediately.
For more information on using the multimap filter see the official `multimap documentation`_ of Rspamd.

View File

@@ -0,0 +1,11 @@
Firewalling
===========
Network flows within Mailu
--------------------------
The following diagram may prove useful in understanding how the different components interact.
.. image:: mailu-network-diagram.svg
:target: ../_images/mailu-network-diagram.svg

View File

@@ -256,8 +256,10 @@ correct syntax. The following file names will be taken as override configuration
- For both ``postfix.cf`` and ``postfix.master``, you need to put one configuration per line, as they are fed line-by-line
to postfix.
- ``logrotate.conf`` as ``$ROOT/overrides/postfix/logrotate.conf`` - Replaces the logrotate.conf file used for rotating ``POSTFIX_LOG_FILE``.
- `Dovecot`_ - ``dovecot.conf`` in dovecot sub-directory;
- `Nginx`_ - All ``*.conf`` files in the ``nginx`` sub-directory;
- `Dovecot`_ - ``dovecot.conf`` in dovecot sub-directory.
- `Nginx`_ :
- All ``*.conf`` files in the ``nginx`` sub-directory.
- ``proxy.conf`` in the ``nginx/dovecot`` sub-directory.
- `Rspamd`_ - All files in the ``rspamd`` sub-directory.
- `Roundcube`_ - All ``*.inc.php`` files in the ``roundcube`` sub directory.

View File

@@ -81,3 +81,4 @@ the version of Mailu that you are running.
contributors/database
contributors/memo
contributors/localization
contributors/firewalling

View File

@@ -0,0 +1,138 @@
digraph mailu {
label = "Mailu network flows";
fontname = "arial";
node [shape = record; fontname = "arial"; fontsize = 8; style = filled; color = "#d3edea";];
splines = "compound";
// node [shape = "box"; fontsize = "10";];
edge [fontsize = 8; arrowsize = 0.5;];
# Components
internet [label = "Internet"; color = "red";];
proxy [label = "Proxy (optional)"; color = "darkorange";];
front [label="Front"; color="dodgerblue";];
admin [label="Admin"; color="green"; fontcolor="white";];
smtp [label="SMTP"; color="orchid";];
redis [label="Redis"; color="turquoise";];
antispam [label="Antispam"; color="magenta";];
antivirus [label="Antivirus"; color="purple"; fontcolor="white";];
imap [label="IMAP"; color="cyan";];
webdav [label="WebDAV"; color="yellow";];
webmail [label="Webmail"; color="darkgoldenrod";];
fetchmail [label="Fetchmail"; color="chocolate";];
oletools [label="Oletools"; color="limegreen";];
fts_attachments [label="Tika"; color="sienna";];
rankdir=LR;
{rank=min; internet};
// {rank=3; proxy};
// {rank=4; front};
// {rank=same; admin smtp redis antispam antivirus imap};
{rank=max; fetchmail};
# Proxy from internet
internet -> proxy [
color="red";
fontcolor="red";
label = <
<TABLE BORDER="0" CELLBORDER="1" CELLPADDING="1">
<TR>
<TD>80/tcp</TD>
<TD>443/tcp</TD>
</TR>
<TR>
<TD>25/tcp</TD>
<TD>465/tcp</TD>
</TR>
<TR>
<TD>587/tcp</TD>
<TD>110/tcp</TD>
</TR>
<TR>
<TD>995/tcp</TD>
<TD>143/tcp</TD>
</TR>
<TR>
<TD>993/tcp</TD>
<TD>4190/tcp</TD>
</TR>
</TABLE>
>;
];
# Front from proxy
proxy -> front [
color="darkorange";
fontcolor="darkorange";
label = <
<TABLE BORDER="0" CELLBORDER="1" CELLPADDING="1">
<TR>
<TD>80/tcp</TD>
<TD>443/tcp</TD>
</TR>
<TR>
<TD>25/tcp</TD>
<TD>465/tcp</TD>
</TR>
<TR>
<TD>587/tcp</TD>
<TD>110/tcp</TD>
</TR>
<TR>
<TD>995/tcp</TD>
<TD>143/tcp</TD>
</TR>
<TR>
<TD>993/tcp</TD>
<TD>4190/tcp</TD>
</TR>
</TABLE>
>;
];
front -> front [label = "8008/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> front [label = "8000/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> admin [label = "8080/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> imap [label = "4190/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> imap [label = "2525/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> imap [label = "143/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> imap [label = "110/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> smtp [label = "25/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> smtp [label = "10025/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> webmail [label = "80/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> antispam [label = "11334/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
front -> webdav [label = "5232/tcp"; color="dodgerblue"; fontcolor="dodgerblue";];
smtp -> admin [label = "8080/tcp"; color="orchid"; fontcolor="orchid";];
smtp -> front [label = "2525/tcp"; color="orchid"; fontcolor="orchid";];
smtp -> antispam [label = "11332/tcp"; color="orchid"; fontcolor="orchid";];
imap -> admin [label = "8080/tcp"; color="cyan"; fontcolor="cyan";];
imap -> antispam [label = "11334/tcp"; color="cyan"; fontcolor="cyan";];
imap -> proxy [label = "25/tcp"; color="cyan"; fontcolor="cyan";];
imap -> fts_attachments [label = "9998/tcp"; color="cyan"; fontcolor="cyan";];
webmail -> front [label = "14190/tcp"; color="darkgoldenrod"; fontcolor="darkgoldenrod";];
webmail -> front [label = "10025/tcp"; color="darkgoldenrod"; fontcolor="darkgoldenrod";];
webmail -> front [label = "10143/tcp"; color="darkgoldenrod"; fontcolor="darkgoldenrod";];
# carddav
webmail -> proxy [label = "443/tcp"; color="darkgoldenrod"; fontcolor="darkgoldenrod";];
admin -> redis [label = "6379/tcp"; color="green"; fontcolor="green";];
admin -> front [label = "2525/tcp"; color="green"; fontcolor="green";];
antispam -> redis [label = "6379/tcp"; color="magenta"; fontcolor="magenta";];
antispam -> admin [label = "8080/tcp"; color="magenta"; fontcolor="magenta";];
antispam -> oletools [label = "11343/tcp"; color="magenta"; fontcolor="magenta";];
antispam -> antivirus [label = "3310/tcp"; color="magenta"; fontcolor="magenta";];
fetchmail -> admin [label = "8080/tcp"; color="chocolate"; fontcolor="chocolate";];
fetchmail -> proxy [label = "25/tcp"; color="chocolate"; fontcolor="chocolate";];
fetchmail -> front [label = "2525/tcp"; color="chocolate"; fontcolor="chocolate";];
#
# those don't need internet:
# oletools
# fts_attachments
# redis
}

View File

@@ -1,517 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 2.43.0 (0)\n",
" -->\n",
"<!-- Title: mailu Pages: 1 -->\n",
"<svg width=\"706pt\" height=\"553pt\"\n",
" viewBox=\"0.00 0.00 706.00 553.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 549)\">\n",
"<title>mailu</title>\n",
"<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-549 702,-549 702,4 -4,4\"/>\n",
"<text text-anchor=\"middle\" x=\"349\" y=\"-7.8\" font-family=\"arial\" font-size=\"14.00\">Mailu</text>\n",
"<!-- internet -->\n",
"<g id=\"node1\" class=\"node\">\n",
"<title>internet</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"297,-545 243,-545 243,-509 297,-509 297,-545\"/>\n",
"<text text-anchor=\"middle\" x=\"270\" y=\"-525.1\" font-family=\"arial\" font-size=\"8.00\">Internet</text>\n",
"</g>\n",
"<!-- front -->\n",
"<g id=\"node2\" class=\"node\">\n",
"<title>front</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"364,-464 310,-464 310,-428 364,-428 364,-464\"/>\n",
"<text text-anchor=\"middle\" x=\"337\" y=\"-444.1\" font-family=\"arial\" font-size=\"8.00\">Front</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge1\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.81,-521.99C180.56,-512.79 33,-491 33,-491 33,-491 33,-482 33,-482 33,-482 218.14,-460.68 299.46,-451.32\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.22,-454.76 309.75,-450.14 299.42,-447.8 300.22,-454.76\"/>\n",
"<text text-anchor=\"middle\" x=\"46.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">80/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge2\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.81,-520.91C190.94,-511.2 83,-491 83,-491 83,-491 83,-482 83,-482 83,-482 228.93,-461.89 299.56,-452.16\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.39,-455.58 309.82,-450.75 299.43,-448.64 300.39,-455.58\"/>\n",
"<text text-anchor=\"middle\" x=\"99\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">443/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge3\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.97,-518.83C204.35,-508.59 138,-491 138,-491 138,-491 138,-482 138,-482 138,-482 241.91,-463.72 299.56,-453.59\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.5,-456.97 309.75,-451.79 299.29,-450.08 300.5,-456.97\"/>\n",
"<text text-anchor=\"middle\" x=\"151.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge4\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.7,-514.35C218.84,-504.16 188,-491 188,-491 188,-491 188,-482 188,-482 188,-482 255.7,-466.1 299.83,-455.73\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.74,-459.11 309.67,-453.42 299.14,-452.3 300.74,-459.11\"/>\n",
"<text text-anchor=\"middle\" x=\"204\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">465/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge5\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M256.82,-508.91C249.96,-500.03 243,-491 243,-491 243,-491 243,-482 243,-482 243,-482 273.97,-470.47 300.36,-460.64\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"301.66,-463.89 309.81,-457.12 299.22,-457.33 301.66,-463.89\"/>\n",
"<text text-anchor=\"middle\" x=\"259\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">587/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge6\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M280.91,-508.86C288.77,-496.51 298,-482 298,-482 298,-482 303.42,-477.13 310.23,-471.03\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"312.85,-473.37 317.96,-464.09 308.18,-468.16 312.85,-473.37\"/>\n",
"<text text-anchor=\"middle\" x=\"314\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">110/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge7\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.25,-517.71C331.26,-507.36 385,-491 385,-491 385,-491 385,-482 385,-482 385,-482 377.6,-476.6 368.6,-470.04\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"370.58,-467.15 360.44,-464.09 366.46,-472.81 370.58,-467.15\"/>\n",
"<text text-anchor=\"middle\" x=\"401\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">995/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge8\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.34,-519.66C340.86,-509.58 421,-491 421,-491 421,-491 421,-482 421,-482 421,-482 396.19,-471.66 373.54,-462.22\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.62,-458.88 364.04,-458.27 371.93,-465.34 374.62,-458.88\"/>\n",
"<text text-anchor=\"middle\" x=\"437\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">143/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge9\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.19,-510.14C313.07,-500.88 330,-491 330,-491 330,-491 331.24,-483.18 332.68,-474.13\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"336.16,-474.56 334.27,-464.14 329.25,-473.46 336.16,-474.56\"/>\n",
"<text text-anchor=\"middle\" x=\"348\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">993/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge10\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.19,-520.91C349.06,-511.2 457,-491 457,-491 457,-491 457,-482 457,-482 457,-482 409.48,-468.14 374.25,-457.86\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.81,-454.38 364.23,-454.94 372.85,-461.1 374.81,-454.38\"/>\n",
"<text text-anchor=\"middle\" x=\"475.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">4190/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;front -->\n",
"<g id=\"edge11\" class=\"edge\">\n",
"<title>front&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.24,-449.75C374.02,-449.83 382,-448.58 382,-446 382,-444.43 379.04,-443.35 374.51,-442.77\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.4,-439.26 364.24,-442.25 374.05,-446.25 374.4,-439.26\"/>\n",
"<text text-anchor=\"middle\" x=\"400.5\" y=\"-444.1\" font-family=\"Times,serif\" font-size=\"8.00\">8008/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;front -->\n",
"<g id=\"edge12\" class=\"edge\">\n",
"<title>front&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.06,-452.31C389.18,-455.1 419,-452.99 419,-446 419,-439.94 396.57,-437.55 374.23,-438.84\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"373.73,-435.37 364.06,-439.69 374.31,-442.34 373.73,-435.37\"/>\n",
"<text text-anchor=\"middle\" x=\"437.5\" y=\"-444.1\" font-family=\"Times,serif\" font-size=\"8.00\">8000/tcp</text>\n",
"</g>\n",
"<!-- admin -->\n",
"<g id=\"node3\" class=\"node\">\n",
"<title>admin</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"556,-302 502,-302 502,-266 556,-266 556,-302\"/>\n",
"<text text-anchor=\"middle\" x=\"529\" y=\"-282.1\" font-family=\"arial\" font-size=\"8.00\">Admin</text>\n",
"</g>\n",
"<!-- front&#45;&gt;admin -->\n",
"<g id=\"edge13\" class=\"edge\">\n",
"<title>front&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.05,-434.12C389.59,-423.84 424,-410 424,-410 424,-410 465,-347 465,-347 465,-347 485.85,-326.8 503.75,-309.46\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"506.43,-311.74 511.17,-302.27 501.55,-306.71 506.43,-311.74\"/>\n",
"<text text-anchor=\"middle\" x=\"483.5\" y=\"-363.1\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"<!-- smtp -->\n",
"<g id=\"node4\" class=\"node\">\n",
"<title>smtp</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"565,-383 511,-383 511,-347 565,-347 565,-383\"/>\n",
"<text text-anchor=\"middle\" x=\"538\" y=\"-363.1\" font-family=\"arial\" font-size=\"8.00\">SMTP</text>\n",
"</g>\n",
"<!-- front&#45;&gt;smtp -->\n",
"<g id=\"edge17\" class=\"edge\">\n",
"<title>front&#45;&gt;smtp</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.19,-439.53C413.12,-429.69 511,-410 511,-410 511,-410 516.33,-401.32 522.25,-391.67\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"525.24,-393.49 527.48,-383.14 519.27,-389.83 525.24,-393.49\"/>\n",
"<text text-anchor=\"middle\" x=\"529.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;smtp -->\n",
"<g id=\"edge18\" class=\"edge\">\n",
"<title>front&#45;&gt;smtp</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.02,-440.5C420.74,-431.04 547,-410 547,-410 547,-410 547,-401 547,-401 547,-401 546.14,-397.65 544.95,-393.02\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"548.27,-391.9 542.39,-383.09 541.5,-393.65 548.27,-391.9\"/>\n",
"<text text-anchor=\"middle\" x=\"568.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">10025/tcp</text>\n",
"</g>\n",
"<!-- antispam -->\n",
"<g id=\"node6\" class=\"node\">\n",
"<title>antispam</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"632,-140 578,-140 578,-104 632,-104 632,-140\"/>\n",
"<text text-anchor=\"middle\" x=\"605\" y=\"-120.1\" font-family=\"arial\" font-size=\"8.00\">Antispam</text>\n",
"</g>\n",
"<!-- front&#45;&gt;antispam -->\n",
"<g id=\"edge20\" class=\"edge\">\n",
"<title>front&#45;&gt;antispam</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.02,-441.32C430,-432.33 594,-410 594,-410 594,-410 649,-248 649,-248 649,-248 649,-239 649,-239 649,-239 620,-158 620,-158 620,-158 618.41,-154.3 616.27,-149.3\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"619.48,-147.9 612.32,-140.09 613.05,-150.66 619.48,-147.9\"/>\n",
"<text text-anchor=\"middle\" x=\"663.5\" y=\"-282.1\" font-family=\"Times,serif\" font-size=\"8.00\">11334/tcp</text>\n",
"</g>\n",
"<!-- imap -->\n",
"<g id=\"node8\" class=\"node\">\n",
"<title>imap</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"411,-221 357,-221 357,-185 411,-185 411,-221\"/>\n",
"<text text-anchor=\"middle\" x=\"384\" y=\"-201.1\" font-family=\"arial\" font-size=\"8.00\">IMAP</text>\n",
"</g>\n",
"<!-- front&#45;&gt;imap -->\n",
"<g id=\"edge14\" class=\"edge\">\n",
"<title>front&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M338.92,-427.88C342.75,-393.65 351,-320 351,-320 351,-320 370,-239 370,-239 370,-239 371.41,-235.47 373.34,-230.66\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"376.7,-231.67 377.16,-221.09 370.2,-229.07 376.7,-231.67\"/>\n",
"<text text-anchor=\"middle\" x=\"369.5\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">4190/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;imap -->\n",
"<g id=\"edge15\" class=\"edge\">\n",
"<title>front&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M309.86,-441.8C236.54,-433.16 40,-410 40,-410 40,-410 0,-383 0,-383 0,-383 0,-347 0,-347 0,-347 286,-239 286,-239 286,-239 319.57,-227.01 347.42,-217.07\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"348.72,-220.32 356.96,-213.66 346.37,-213.72 348.72,-220.32\"/>\n",
"<text text-anchor=\"middle\" x=\"87\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">143/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;imap -->\n",
"<g id=\"edge16\" class=\"edge\">\n",
"<title>front&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M346.28,-427.91C351.1,-419.03 356,-410 356,-410 356,-410 388,-329 388,-329 388,-329 388,-320 388,-320 388,-320 386.13,-265.76 384.95,-231.46\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"388.43,-230.99 384.59,-221.12 381.44,-231.23 388.43,-230.99\"/>\n",
"<text text-anchor=\"middle\" x=\"404\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">110/tcp</text>\n",
"</g>\n",
"<!-- webdav -->\n",
"<g id=\"node9\" class=\"node\">\n",
"<title>webdav</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"63,-383 9,-383 9,-347 63,-347 63,-383\"/>\n",
"<text text-anchor=\"middle\" x=\"36\" y=\"-363.1\" font-family=\"arial\" font-size=\"8.00\">WebDAV</text>\n",
"</g>\n",
"<!-- front&#45;&gt;webdav -->\n",
"<g id=\"edge21\" class=\"edge\">\n",
"<title>front&#45;&gt;webdav</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M309.79,-441.74C237.3,-433.05 45,-410 45,-410 45,-410 43.4,-402.18 41.55,-393.13\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"44.94,-392.23 39.51,-383.14 38.08,-393.64 44.94,-392.23\"/>\n",
"<text text-anchor=\"middle\" x=\"63.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">5232/tcp</text>\n",
"</g>\n",
"<!-- webmail -->\n",
"<g id=\"node10\" class=\"node\">\n",
"<title>webmail</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"211,-383 157,-383 157,-347 211,-347 211,-383\"/>\n",
"<text text-anchor=\"middle\" x=\"184\" y=\"-363.1\" font-family=\"arial\" font-size=\"8.00\">Webmail</text>\n",
"</g>\n",
"<!-- front&#45;&gt;webmail -->\n",
"<g id=\"edge19\" class=\"edge\">\n",
"<title>front&#45;&gt;webmail</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M310,-437.06C274.89,-426.73 218,-410 218,-410 218,-410 211.02,-400.97 203.39,-391.09\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"206.13,-388.91 197.24,-383.14 200.59,-393.19 206.13,-388.91\"/>\n",
"<text text-anchor=\"middle\" x=\"231.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">80/tcp</text>\n",
"</g>\n",
"<!-- redis -->\n",
"<g id=\"node5\" class=\"node\">\n",
"<title>redis</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"537,-59 483,-59 483,-23 537,-23 537,-59\"/>\n",
"<text text-anchor=\"middle\" x=\"510\" y=\"-39.1\" font-family=\"arial\" font-size=\"8.00\">Redis</text>\n",
"</g>\n",
"<!-- admin&#45;&gt;redis -->\n",
"<g id=\"edge32\" class=\"edge\">\n",
"<title>admin&#45;&gt;redis</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M522.36,-265.88C509.84,-233.62 484,-167 484,-167 484,-167 484,-158 484,-158 484,-158 496.26,-103.29 503.95,-69.01\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"507.39,-69.64 506.16,-59.12 500.56,-68.11 507.39,-69.64\"/>\n",
"<text text-anchor=\"middle\" x=\"502.5\" y=\"-160.6\" font-family=\"Times,serif\" font-size=\"8.00\">6379/tcp</text>\n",
"</g>\n",
"<!-- admin&#45;&gt;imap -->\n",
"<g id=\"edge33\" class=\"edge\">\n",
"<title>admin&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M501.75,-274.71C467.74,-264.36 414,-248 414,-248 414,-248 408.08,-239.32 401.5,-229.67\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"404.21,-227.43 395.68,-221.14 398.43,-231.37 404.21,-227.43\"/>\n",
"<text text-anchor=\"middle\" x=\"432.5\" y=\"-241.6\" font-family=\"Times,serif\" font-size=\"8.00\">2525/tcp</text>\n",
"</g>\n",
"<!-- smtp&#45;&gt;front -->\n",
"<g id=\"edge23\" class=\"edge\">\n",
"<title>smtp&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M510.95,-379.68C485.41,-392.6 451,-410 451,-410 451,-410 407.25,-423.43 373.95,-433.65\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"372.55,-430.42 364.02,-436.71 374.6,-437.12 372.55,-430.42\"/>\n",
"<text text-anchor=\"middle\" x=\"485.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">2525/tcp</text>\n",
"</g>\n",
"<!-- smtp&#45;&gt;admin -->\n",
"<g id=\"edge22\" class=\"edge\">\n",
"<title>smtp&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M536.05,-346.86C534.89,-336.71 533.4,-323.63 532.09,-312.12\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"535.56,-311.65 530.95,-302.11 528.6,-312.44 535.56,-311.65\"/>\n",
"<text text-anchor=\"middle\" x=\"551.5\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"<!-- smtp&#45;&gt;antispam -->\n",
"<g id=\"edge24\" class=\"edge\">\n",
"<title>smtp&#45;&gt;antispam</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M555.58,-346.91C564.72,-338.03 574,-329 574,-329 574,-329 608,-221 608,-221 608,-221 608,-185 608,-185 608,-185 607.12,-166.91 606.32,-150.27\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"609.81,-150.09 605.84,-140.27 602.82,-150.43 609.81,-150.09\"/>\n",
"<text text-anchor=\"middle\" x=\"623.5\" y=\"-241.6\" font-family=\"Times,serif\" font-size=\"8.00\">11332/tcp</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;admin -->\n",
"<g id=\"edge35\" class=\"edge\">\n",
"<title>antispam&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M597.99,-140.14C592.94,-152.49 587,-167 587,-167 587,-167 559.17,-222.66 542.07,-256.87\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"538.9,-255.37 537.56,-265.88 545.16,-258.5 538.9,-255.37\"/>\n",
"<text text-anchor=\"middle\" x=\"590.5\" y=\"-201.1\" font-family=\"Times,serif\" font-size=\"8.00\">80/tcp</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;redis -->\n",
"<g id=\"edge34\" class=\"edge\">\n",
"<title>antispam&#45;&gt;redis</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M583.03,-103.91C571.6,-95.03 560,-86 560,-86 560,-86 548.91,-76.24 537.17,-65.91\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"539.29,-63.12 529.47,-59.14 534.67,-68.37 539.29,-63.12\"/>\n",
"<text text-anchor=\"middle\" x=\"578.5\" y=\"-79.6\" font-family=\"Times,serif\" font-size=\"8.00\">6379/tcp</text>\n",
"</g>\n",
"<!-- antivirus -->\n",
"<g id=\"node7\" class=\"node\">\n",
"<title>antivirus</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"698,-59 644,-59 644,-23 698,-23 698,-59\"/>\n",
"<text text-anchor=\"middle\" x=\"671\" y=\"-39.1\" font-family=\"arial\" font-size=\"8.00\">Anti&#45;Virus</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;antivirus -->\n",
"<g id=\"edge37\" class=\"edge\">\n",
"<title>antispam&#45;&gt;antivirus</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M626.48,-103.91C637.65,-95.03 649,-86 649,-86 649,-86 653.17,-77.67 657.87,-68.26\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"661.09,-69.65 662.43,-59.14 654.83,-66.52 661.09,-69.65\"/>\n",
"<text text-anchor=\"middle\" x=\"671.5\" y=\"-79.6\" font-family=\"Times,serif\" font-size=\"8.00\">3310/tcp</text>\n",
"</g>\n",
"<!-- oletools -->\n",
"<g id=\"node12\" class=\"node\">\n",
"<title>oletools</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"626,-59 572,-59 572,-23 626,-23 626,-59\"/>\n",
"<text text-anchor=\"middle\" x=\"599\" y=\"-39.1\" font-family=\"arial\" font-size=\"8.00\">Oletools</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;oletools -->\n",
"<g id=\"edge36\" class=\"edge\">\n",
"<title>antispam&#45;&gt;oletools</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M603.7,-103.86C602.93,-93.71 601.93,-80.63 601.06,-69.12\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"604.55,-68.81 600.3,-59.11 597.57,-69.34 604.55,-68.81\"/>\n",
"<text text-anchor=\"middle\" x=\"623.5\" y=\"-79.6\" font-family=\"Times,serif\" font-size=\"8.00\">11343/tcp</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;front -->\n",
"<g id=\"edge27\" class=\"edge\">\n",
"<title>imap&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M374.23,-221.09C369.16,-229.97 364,-239 364,-239 364,-239 320,-320 320,-320 320,-320 320,-329 320,-329 320,-329 328.02,-383.71 333.04,-417.99\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"329.58,-418.5 334.49,-427.88 336.5,-417.48 329.58,-418.5\"/>\n",
"<text text-anchor=\"middle\" x=\"333.5\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;admin -->\n",
"<g id=\"edge25\" class=\"edge\">\n",
"<title>imap&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M411.25,-217.43C431.27,-227.3 455,-239 455,-239 455,-239 474.19,-250.41 493.02,-261.61\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"491.48,-264.76 501.87,-266.87 495.06,-258.75 491.48,-264.76\"/>\n",
"<text text-anchor=\"middle\" x=\"487.5\" y=\"-241.6\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;antispam -->\n",
"<g id=\"edge26\" class=\"edge\">\n",
"<title>imap&#45;&gt;antispam</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M411.19,-195.25C452.16,-185.08 525,-167 525,-167 525,-167 547.74,-154.49 568.98,-142.81\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"570.84,-145.78 577.92,-137.9 567.47,-139.65 570.84,-145.78\"/>\n",
"<text text-anchor=\"middle\" x=\"561.5\" y=\"-160.6\" font-family=\"Times,serif\" font-size=\"8.00\">11334/tcp</text>\n",
"</g>\n",
"<!-- fts_attachments -->\n",
"<g id=\"node13\" class=\"node\">\n",
"<title>fts_attachments</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"411,-140 357,-140 357,-104 411,-104 411,-140\"/>\n",
"<text text-anchor=\"middle\" x=\"384\" y=\"-120.1\" font-family=\"arial\" font-size=\"8.00\">Tika</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;fts_attachments -->\n",
"<g id=\"edge28\" class=\"edge\">\n",
"<title>imap&#45;&gt;fts_attachments</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M384,-184.86C384,-174.71 384,-161.63 384,-150.12\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"387.5,-150.11 384,-140.11 380.5,-150.11 387.5,-150.11\"/>\n",
"<text text-anchor=\"middle\" x=\"402.5\" y=\"-160.6\" font-family=\"Times,serif\" font-size=\"8.00\">9998/tcp</text>\n",
"</g>\n",
"<!-- webmail&#45;&gt;front -->\n",
"<g id=\"edge29\" class=\"edge\">\n",
"<title>webmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M211.04,-380.16C235.6,-393.03 268,-410 268,-410 268,-410 283.98,-418.1 300.56,-426.51\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"299.23,-429.76 309.73,-431.17 302.39,-423.52 299.23,-429.76\"/>\n",
"<text text-anchor=\"middle\" x=\"289.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">14190/tcp</text>\n",
"</g>\n",
"<!-- webmail&#45;&gt;front -->\n",
"<g id=\"edge30\" class=\"edge\">\n",
"<title>webmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M156.96,-375.66C127.95,-386.02 86,-401 86,-401 86,-401 86,-410 86,-410 86,-410 229.5,-430.01 299.49,-439.77\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"299.27,-443.27 309.66,-441.19 300.24,-436.34 299.27,-443.27\"/>\n",
"<text text-anchor=\"middle\" x=\"107.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">10025/tcp</text>\n",
"</g>\n",
"<!-- webmail&#45;&gt;front -->\n",
"<g id=\"edge31\" class=\"edge\">\n",
"<title>webmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M174.72,-383.09C169.9,-391.97 165,-401 165,-401 165,-401 165,-410 165,-410 165,-410 249.24,-427.14 299.81,-437.43\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"299.25,-440.89 309.75,-439.45 300.64,-434.03 299.25,-440.89\"/>\n",
"<text text-anchor=\"middle\" x=\"186.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">10143/tcp</text>\n",
"</g>\n",
"<!-- fetchmail -->\n",
"<g id=\"node11\" class=\"node\">\n",
"<title>fetchmail</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"654,-545 600,-545 600,-509 654,-509 654,-545\"/>\n",
"<text text-anchor=\"middle\" x=\"627\" y=\"-525.1\" font-family=\"arial\" font-size=\"8.00\">Fetchmail</text>\n",
"</g>\n",
"<!-- fetchmail&#45;&gt;front -->\n",
"<g id=\"edge39\" class=\"edge\">\n",
"<title>fetchmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M599.96,-516.78C562.13,-503.87 498,-482 498,-482 498,-482 421.83,-465.44 374.28,-455.1\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.83,-451.64 364.31,-452.94 373.34,-458.48 374.83,-451.64\"/>\n",
"<text text-anchor=\"middle\" x=\"535.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- fetchmail&#45;&gt;front -->\n",
"<g id=\"edge40\" class=\"edge\">\n",
"<title>fetchmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M599.87,-509.87C578.75,-497.31 553,-482 553,-482 553,-482 436.36,-463.1 374.51,-453.08\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.79,-449.58 364.36,-451.43 373.67,-456.49 374.79,-449.58\"/>\n",
"<text text-anchor=\"middle\" x=\"585.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">2525/tcp</text>\n",
"</g>\n",
"<!-- fetchmail&#45;&gt;admin -->\n",
"<g id=\"edge38\" class=\"edge\">\n",
"<title>fetchmail&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M622.57,-508.79C609.83,-459.28 574,-320 574,-320 574,-320 567.2,-314.71 558.88,-308.24\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"561.02,-305.47 550.97,-302.09 556.72,-310.99 561.02,-305.47\"/>\n",
"<text text-anchor=\"middle\" x=\"614.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.sources.Source at 0x7f2c4e69e690>"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import graphviz\n",
"\n",
"a = \"\"\"\n",
"digraph mailu {\n",
" label = \"Mailu\";\n",
" fontname = \"arial\";\n",
" \n",
" node [shape = box; fontname = \"arial\"; fontsize = 8; style = filled; color = \"#d3edea\";];\n",
" splines = \"compound\";\n",
" // node [shape = \"box\"; fontsize = \"10\";];\n",
" edge [fontsize = \"8\";];\n",
" \n",
" # Components\n",
" internet [label = \"Internet\";];\n",
" front [label = \"Front\";];\n",
" admin [label = \"Admin\";];\n",
" smtp [label = \"SMTP\";];\n",
" redis [label = \"Redis\";];\n",
" antispam [label = \"Antispam\";];\n",
" antivirus [label = \"Anti-Virus\";];\n",
" imap [label = \"IMAP\";];\n",
" webdav [label = \"WebDAV\";];\n",
" webmail [label = \"Webmail\";];\n",
" fetchmail [label = \"Fetchmail\";];\n",
" oletools [label = \"Oletools\"];\n",
" fts_attachments [label = \"Tika\"];\n",
" \n",
" # Front from internet\n",
" internet -> front [label = \"80/tcp\";];\n",
" internet -> front [label = \"443/tcp\";];\n",
" internet -> front [label = \"25/tcp\";];\n",
" internet -> front [label = \"465/tcp\";];\n",
" internet -> front [label = \"587/tcp\";];\n",
" internet -> front [label = \"110/tcp\";];\n",
" internet -> front [label = \"995/tcp\";];\n",
" internet -> front [label = \"143/tcp\";];\n",
" internet -> front [label = \"993/tcp\";];\n",
" internet -> front [label = \"4190/tcp\";];\n",
" \n",
" front -> front [label = \"8008/tcp\";];\n",
" front -> front [label = \"8000/tcp\";];\n",
" front -> admin [label = \"8080/tcp\";];\n",
" front -> imap [label = \"4190/tcp\";];\n",
" front -> imap [label = \"143/tcp\";];\n",
" front -> imap [label = \"110/tcp\";];\n",
" front -> smtp [label = \"25/tcp\";];\n",
" front -> smtp [label = \"10025/tcp\";];\n",
" front -> webmail [label = \"80/tcp\";];\n",
" front -> antispam [label = \"11334/tcp\";];\n",
" front -> webdav [label = \"5232/tcp\";];\n",
" \n",
" smtp -> admin [label = \"8080/tcp\";];\n",
" smtp -> front [label = \"2525/tcp\";];\n",
" smtp -> antispam [label = \"11332/tcp\";];\n",
" \n",
" imap -> admin [label = \"8080/tcp\";];\n",
" imap -> antispam [label = \"11334/tcp\";];\n",
" imap -> front [label = \"25/tcp\";];\n",
" imap -> fts_attachments [label = \"9998/tcp\";];\n",
" \n",
" webmail -> front [label = \"14190/tcp\";];\n",
" webmail -> front [label = \"10025/tcp\";];\n",
" webmail -> front [label = \"10143/tcp\";];\n",
" \n",
" admin -> redis [label = \"6379/tcp\";];\n",
" admin -> imap [label = \"2525/tcp\";];\n",
" \n",
" antispam -> redis [label = \"6379/tcp\";];\n",
" antispam -> admin [label = \"80/tcp\";];\n",
" antispam -> oletools [label = \"11343/tcp\";];\n",
" antispam -> antivirus [label = \"3310/tcp\";];\n",
" \n",
" fetchmail -> admin [label = \"8080/tcp\"]\n",
" fetchmail -> front [label = \"25/tcp\"]\n",
" fetchmail -> front [label = \"2525/tcp\"]\n",
" #\n",
" # those don't need internet:\n",
" # oletools\n",
" # fts_attachments\n",
" # redis\n",
"}\n",
"\"\"\"\n",
"\n",
"dot = graphviz.Source(a)\n",
"dot\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -46,7 +46,8 @@ In the case of *certbot* you could write a script to be executed as `deploy hook
#!/bin/sh
cp /etc/letsencrypt/live/domain.com/privkey.pem /mailu/certs/key.pem || exit 1
cp /etc/letsencrypt/live/domain.com/fullchain.pem /mailu/certs/cert.pem || exit 1
docker exec mailu_front_1 nginx -s reload
docker exec mailu-front-1 nginx -s reload
docker exec mailu-front-1 doveadm reload
And the certbot command you will use in crontab would look something like:

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import binascii
import time
import os
from pathlib import Path
@@ -32,6 +33,19 @@ poll "{host}" proto {protocol} port {port}
{lmtp}
"""
def imaputf7encode(s):
"""Encode a string into RFC2060 aka IMAP UTF7"""
out = ''
enc = ''
for c in s.replace('&','&-') + 'X':
if '\x20' <= c <= '\x7f':
if enc:
out += f'&{binascii.b2a_base64(enc.encode("utf-16-be")).rstrip(b"\n=").replace(b"/", b",").decode("ascii")}-'
enc = ''
out += c
else:
enc += c
return out[:-1]
def escape_rc_string(arg):
return "".join("\\x%2x" % ord(char) for char in arg)
@@ -54,13 +68,13 @@ def run(debug):
options = "options antispam 501, 504, 550, 553, 554"
options += " ssl" if fetch["tls"] else ""
options += " keep" if fetch["keep"] else " fetchall"
folders = "folders %s" % ((','.join('"' + item + '"' for item in fetch['folders'])) if fetch['folders'] else '"INBOX"')
folders = f"folders {",".join(f'"{imaputf7encode(item).replace('"',r"\34")}"' for item in fetch["folders"]) or '"INBOX"'}"
fetchmailrc += RC_LINE.format(
user_email=escape_rc_string(fetch["user_email"]),
protocol=fetch["protocol"],
host=escape_rc_string(fetch["host"]),
port=fetch["port"],
smtphost=f'{os.environ["FRONT_ADDRESS"]}' if fetch['scan'] else f'{os.environ["FRONT_ADDRESS"]}/2525',
smtphost=f'{os.environ["HOSTNAMES"].split(",")[0]}' if fetch['scan'] and os.environ.get('PROXY_PROTOCOL_25', False) else f'{os.environ["FRONT_ADDRESS"]}' if fetch['scan'] else f'{os.environ["FRONT_ADDRESS"]}/2525',
username=escape_rc_string(fetch["username"]),
password=escape_rc_string(fetch["password"]),
options=options,

View File

@@ -1,6 +1,6 @@
[server]
ssl = False
hosts = 0.0.0.0:5232
hosts = 0.0.0.0:5232, [::]:5232
[encoding]
request = utf-8

View File

@@ -15,7 +15,7 @@ COPY main.py ./main.py
RUN echo $VERSION >> /version
EXPOSE 80/tcp
HEALTHCHECK --start-period=350s CMD curl -skfLo /dev/null http://localhost/
HEALTHCHECK --start-period=350s CMD curl -m3 -skfLo /dev/null http://localhost/
USER mailu
CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app

View File

@@ -217,7 +217,7 @@ services:
# Optional services
{% if antivirus_enabled %}
antivirus:
image: clamav/clamav-debian:1.2.0-6
image: clamav/clamav-debian:1.2.3-45
restart: always
logging:
driver: journald

View File

@@ -8,5 +8,5 @@ docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mail
docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu admin admin mailu.io 'password' --mode=update || exit 1
docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user user mailu.io 'password' || exit 1
docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user 'user/with/slash' mailu.io 'password' || exit 1
docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user 'user_UTF8' mailu.io 'password€' || exit 1
docker compose -f tests/compose/core/docker-compose.yml exec -T admin flask mailu user 'user_UTF8' mailu.io 'pass%e9word€' || exit 1
echo "User testing successful!"

View File

@@ -8,7 +8,7 @@ import managesieve
SERVER='localhost'
USERNAME='user_UTF8@mailu.io'
PASSWORD='password€'
PASSWORD='pass%e9word€'
#https://github.com/python/cpython/issues/73936
#SMTPlib does not support UTF8 passwords.
USERNAME_ASCII='user@mailu.io'
@@ -139,4 +139,4 @@ if __name__ == '__main__':
test_SMTP(SERVER, USERNAME_ASCII, PASSWORD_ASCII)
test_managesieve(SERVER, USERNAME, PASSWORD)
#https://github.com/python/cpython/issues/73936
#SMTPlib does not support UTF8 passwords.
#SMTPlib does not support UTF8 passwords.

View File

@@ -0,0 +1 @@
Ensure fetchmail can deal with special characters in folder names

View File

@@ -0,0 +1 @@
Increase the size of buffers for webmail

View File

@@ -0,0 +1,5 @@
Update to a newer clamav 1.2.3-45
Update to snappymail 2.36.4
Update to roundcube 1.6.8 (CVE-2024-42009, CVE-2024-42008, CVE-2024-42010)
Add a new DNS entry for autodiscover (old MUA autoconfiguration)
Clarify the language in the documentation related to rspamd overrides

View File

@@ -0,0 +1 @@
Fix email-forwarding when set from the web interface

View File

@@ -0,0 +1 @@
Fix a bug preventing percent characters from being used in passwords

View File

@@ -0,0 +1 @@
Fix #3379: DEFAULT_QUOTA

View File

@@ -0,0 +1 @@
Ensure that file:// protocol is not allowed in CURL

View File

@@ -0,0 +1 @@
Update roundcube to 1.6.9

View File

@@ -0,0 +1,2 @@
Disable HARDENED_MALLOC unless the requirements are met
Ensure the healthchecks timeout

View File

@@ -0,0 +1 @@
Fix an error that can occur when using snappymail

View File

@@ -0,0 +1 @@
Fix a potential problem with SO_REUSEADDR that may prevent admin from starting up

View File

@@ -0,0 +1 @@
fix INBOUND_TLS_ENFORCE

View File

@@ -0,0 +1 @@
Update the documentation: ensure that users reload dovecot too if they manually configure certificates

View File

@@ -0,0 +1 @@
Ensure we do not nuke all web-sessions when a password is changed using the command line

View File

@@ -0,0 +1 @@
The reload functionality of nginx/dovecot upon change of the certificates failed with an error.

View File

@@ -0,0 +1,2 @@
Ensure we can do more than 100 parallel sessions.
Allow dovecot's config to be overriden in front too

View File

@@ -0,0 +1 @@
Fix broken overrides in 2024.06.17

View File

@@ -0,0 +1 @@
Ensure we have both RSA and ECDSA certs when using letsencrypt

View File

@@ -0,0 +1 @@
HTTP/2 does not require ipv6; in fact it does not require SSL certs either

View File

@@ -0,0 +1 @@
Filter logs line based and in binary mode without decoding utf-8

View File

@@ -0,0 +1 @@
Upgrade to alpine 3.20.3

View File

@@ -0,0 +1 @@
Upgrade snappymail to v2.38.2 ; this is a security fix for GHSA-2rq7-79vp-ffxm (mXSS)

View File

@@ -0,0 +1 @@
Don't check empty passwords against HIBP

View File

@@ -28,7 +28,7 @@ RUN set -euxo pipefail \
; mkdir -p /run/nginx /conf
# roundcube
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.6.7/roundcubemail-1.6.7-complete.tar.gz
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.6.9/roundcubemail-1.6.9-complete.tar.gz
ENV CARDDAV_URL https://github.com/mstilkerich/rcmcarddav/releases/download/v5.1.0/carddav-v5.1.0.tar.gz
RUN set -euxo pipefail \
@@ -54,7 +54,7 @@ COPY roundcube/config/config.inc.carddav.php /var/www/roundcube/plugins/carddav/
# snappymail
ENV SNAPPYMAIL_URL https://github.com/the-djmaze/snappymail/releases/download/v2.36.1/snappymail-2.36.1.tar.gz
ENV SNAPPYMAIL_URL https://github.com/the-djmaze/snappymail/releases/download/v2.38.2/snappymail-2.38.2.tar.gz
RUN set -euxo pipefail \
; mkdir /var/www/snappymail \
@@ -94,6 +94,6 @@ VOLUME /overrides
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ping || exit 1
HEALTHCHECK CMD curl -m3 -f -L http://localhost/ping || exit 1
RUN echo $VERSION >> /version

View File

@@ -55,6 +55,16 @@ server {
{% else %}
fastcgi_param SCRIPT_NAME {{WEB_WEBMAIL}}/$fastcgi_script_name;
{% endif %}
# fastcgi buffers for php-fpm #
fastcgi_buffers 16 32k;
fastcgi_buffer_size 64k;
fastcgi_busy_buffers_size 64k;
# nginx buffers #
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
location ~ (^|/)\. {

View File

@@ -39,7 +39,9 @@ sp.disable_function.function("chmod").param("permissions").value("438").drop();
sp.disable_function.function("chmod").param("permissions").value("511").drop();
# Prevent various `mail`-related vulnerabilities
# Uncommend the second rule if you're using php8.3+
sp.disable_function.function("mail").param("additional_parameters").value_r("\\-").drop();
sp.disable_function.function("mail").param("additional_params").value_r("\\-").drop();
# Since it's now burned, me might as well mitigate it publicly
sp.disable_function.function("putenv").param("assignment").value_r("LD_").drop()
@@ -52,8 +54,7 @@ sp.disable_function.function("putenv").param("assignment").value_r("GCONV_").dro
sp.disable_function.function("extract").param("array").value_r("^_").drop()
sp.disable_function.function("extract").param("flags").value("0").drop()
# This is also burned:
# ini_set('open_basedir','..');chdir('..');…;chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/etc/passwd'));
# See https://dustri.org/b/ini_set-based-open_basedir-bypass.html
# Since we have no way of matching on two parameters at the same time, we're
# blocking calls to open_basedir altogether: nobody is using it via ini_set anyway.
# Moreover, there are non-public bypasses that are also using this vector ;)
@@ -119,12 +120,17 @@ sp.disable_function.function("curl_setopt").param("value").value("2").allow();
sp.disable_function.function("curl_setopt").param("option").value("64").drop().alias("Please don't turn CURLOPT_SSL_VERIFYCLIENT off.");
sp.disable_function.function("curl_setopt").param("option").value("81").drop().alias("Please don't turn CURLOPT_SSL_VERIFYHOST off.");
# Ensure that file:// protocol is not allowed in CURL
sp.disable_function.function("curl_setopt").param("value").value_r("file://").drop().alias("file:// protocol is disabled");
sp.disable_function.function("curl_init").param("url").value_r("file://").drop().alias("file:// protocol is disabled");
# File upload
sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ph").drop();
sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ht").drop();
# Logging lockdown
sp.disable_function.function("ini_set").param("option").value_r("error_log").drop()
sp.disable_function.function("ini_set").param("option").value_r("display_errors").filename_r("/var/www/snappymail/snappymail/v/[0-9]+\.[0-9]+\.[0-9]+/app/libraries/snappymail/shutdown.php").allow();
sp.disable_function.function("ini_set").param("option").value_r("display_errors").drop()
sp.auto_cookie_secure.enable();