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/lib*
/docs/bin /docs/bin
/docs/include /docs/include
/docs/contributors/mailu-network-diagram.svg
/docs/_build /docs/_build
/.env /.env
/.venv /.venv

View File

@@ -23,7 +23,7 @@ RUN set -euxo pipefail \
RUN echo $VERSION >/version RUN echo $VERSION >/version
#EXPOSE 8080/tcp #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"] VOLUME ["/data","/dkim"]

View File

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

View File

@@ -18,7 +18,11 @@ STATUSES = {
"sieve": "AuthFailed" "sieve": "AuthFailed"
}), }),
"encryption": ("Must issue a STARTTLS command first", { "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)", { "ratelimit": ("Temporary authentication failure (rate-limit)", {
"imap": "LIMIT", "imap": "LIMIT",
@@ -68,7 +72,7 @@ def handle_authentication(headers):
# Incoming mail, no authentication # Incoming mail, no authentication
if method in ['', 'none'] and protocol in ['smtp', 'lmtp']: if method in ['', 'none'] and protocol in ['smtp', 'lmtp']:
server, port = get_server(protocol, False) 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": if "Auth-SSL" in headers and headers["Auth-SSL"] == "on":
return { return {
"Auth-Status": "OK", "Auth-Status": "OK",
@@ -91,20 +95,14 @@ def handle_authentication(headers):
# Authenticated user # Authenticated user
elif method in ['plain', 'login']: elif method in ['plain', 'login']:
is_valid_user = False 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' user_email = 'invalid'
password = 'invalid' password = 'invalid'
try: try:
user_email = raw_user_email.encode("iso8859-1").decode("utf8") user_email = urllib.parse.unquote(headers["Auth-User"])
password = raw_password.encode("iso8859-1").decode("utf8") password = urllib.parse.unquote(headers["Auth-Pass"])
ip = urllib.parse.unquote(headers["Client-Ip"]) ip = urllib.parse.unquote(headers["Client-Ip"])
except: 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: else:
try: try:
user = models.User.query.get(user_email) if '@' in user_email else None 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-Status'] = status
response.headers['Auth-Error-Code'] = code response.headers['Auth-Error-Code'] = code
return response return response
raw_password = urllib.parse.unquote(headers['Auth-Pass']) if 'Auth-Pass' in headers else ''
headers = nginx.handle_authentication(flask.request.headers) headers = nginx.handle_authentication(flask.request.headers)
response = flask.Response() response = flask.Response()
for key, value in headers.items(): for key, value in headers.items():
@@ -50,13 +49,7 @@ def nginx_authentication():
if not is_port_25: if not is_port_25:
utils.limiter.exempt_ip_from_ratelimits(client_ip) utils.limiter.exempt_ip_from_ratelimits(client_ip)
elif is_valid_user: elif is_valid_user:
password = None password = urllib.parse.unquote(headers.get('Auth-Pass', ''))
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) utils.limiter.rate_limit_user(username, client_ip, password=password)
elif not is_from_webmail: elif not is_from_webmail:
utils.limiter.rate_limit_ip(client_ip, username) utils.limiter.rate_limit_ip(client_ip, username)

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 .' 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 for proto, port, prio
in protocols 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 @cached_property
def dns_tlsa(self): def dns_tlsa(self):
@@ -680,7 +680,7 @@ in clear-text regardless of the presence of the cache.
set() containing the sessions to keep set() containing the sessions to keep
""" """
self.password = password if raw else User.get_password_context().hash(password) 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) utils.MailuSessionExtension.prune_sessions(uid=self.email, keep=keep_sessions)
def get_managed_domains(self): 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)) flask.url_for('.user_list', domain_name=domain.name))
form = forms.UserForm() form = forms.UserForm()
form.pw.validators = [wtforms.validators.DataRequired()] 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: if domain.max_quota_bytes:
form.quota_bytes.validators = [ form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=domain.max_quota_bytes)] wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
@@ -93,11 +93,11 @@ def user_settings(user_email):
form = forms.UserSettingsForm(obj=user) form = forms.UserSettingsForm(obj=user)
utils.formatCSVField(form.forward_destination) utils.formatCSVField(form.forward_destination)
if form.validate_on_submit(): 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') flask.flash('Destination email address is missing', 'error')
user.forward_enabled = True return flask.redirect(
return flask.render_template('user/settings.html', form=form, user=user) flask.url_for('.user_settings', user_email=user_email))
if form.forward_enabled.data:
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",") form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
form.populate_obj(user) form.populate_obj(user)
models.db.session.commit() models.db.session.commit()
@@ -107,8 +107,9 @@ def user_settings(user_email):
return flask.redirect( return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name)) flask.url_for('.user_list', domain_name=user.domain.name))
elif form.is_submitted() and not form.validate(): elif form.is_submitted() and not form.validate():
user.forward_enabled = form.forward_enabled.data flask.flash('Error validating the form', 'error')
return flask.render_template('user/settings.html', form=form, user=user) return flask.redirect(
flask.url_for('.user_settings', user_email=user_email))
return flask.render_template('user/settings.html', form=form, user=user) return flask.render_template('user/settings.html', form=form, user=user)
def _process_password_change(form, user_email): def _process_password_change(form, user_email):

View File

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

View File

@@ -64,7 +64,7 @@ test_unsupported()
cmdline = [ cmdline = [
"gunicorn", "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 # 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", "-b", f"{'[::]' if os.environ.get('SUBNET6') else '0.0.0.0'}:8080",
"--logger-class mailu.Logger", "--logger-class mailu.Logger",

View File

@@ -3,7 +3,7 @@
# base system image (intermediate) # base system image (intermediate)
# Note when updating the alpine tag, first manually run the workflow .github/workflows/mirror.yml. # 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. # 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 FROM $DISTRO as system
ENV TZ=Etc/UTC LANG=C.UTF-8 ENV TZ=Etc/UTC LANG=C.UTF-8

View File

@@ -32,29 +32,28 @@ def _coerce_value(value):
class LogFilter(object): class LogFilter(object):
def __init__(self, stream, re_patterns): def __init__(self, stream, re_patterns):
self.stream = stream self.stream = stream
if isinstance(re_patterns, list): self.pattern = re.compile(b'|'.join([b''.join([b'(?:', pattern, b')']) for pattern in re_patterns]))
self.pattern = re.compile('|'.join([fr'(?:{pattern})' for pattern in re_patterns])) self.buffer = b''
elif isinstance(re_patterns, str):
self.pattern = re.compile(re_patterns)
else:
self.pattern = re_patterns
self.found = False
def __getattr__(self, attr_name): def __getattr__(self, attr_name):
return getattr(self.stream, attr_name) return getattr(self.stream, attr_name)
def write(self, data): def write(self, data):
if data == '\n' and self.found: if type(data) is str:
self.found = False data = data.encode('utf-8')
else: self.buffer += data
if not self.pattern.search(data): while b'\n' in self.buffer:
self.stream.write(data) 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() self.stream.flush()
else: self.buffer = rest
# caught bad pattern
self.found = True
def flush(self): 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() self.stream.flush()
def _is_compatible_with_hardened_malloc(): def _is_compatible_with_hardened_malloc():
@@ -100,7 +99,7 @@ def set_env(required_secrets=[], log_filters=[]):
for secret in required_secrets: for secret in required_secrets:
os.environ[f'{secret}_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray(secret, 'utf-8'), 'sha256').hexdigest() 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 { return {
key: _coerce_value(os.environ.get(key, value)) key: _coerce_value(os.environ.get(key, value))
@@ -108,8 +107,41 @@ def set_env(required_secrets=[], log_filters=[]):
} }
def clean_env(): 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")] [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'): def drop_privs_to(username='mailu'):
pwnam = getpwnam(username) 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 # 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): 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 = threading.Thread(target=forward_text_lines, args=(process.stdout, sys.stdout))
stdout_thread.daemon = True stdout_thread.daemon = True
@@ -137,4 +169,7 @@ def run_process_and_forward_output(cmd):
stderr_thread.daemon = True stderr_thread.daemon = True
stderr_thread.start() 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 { inet_listener imap {
port = 143 port = 143
} }
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
} }
service pop3-login { service pop3-login {
inet_listener pop3 { inet_listener pop3 {
port = 110 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 { inet_listener sieve {
port = 4190 port = 4190
} }
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
} }
protocol sieve { protocol sieve {

View File

@@ -7,7 +7,9 @@ import multiprocessing
from podop import run_server from podop import run_server
from socrate import system, conf 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(): def start_podop():
system.drop_privs_to('mail') 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 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 # 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"] VOLUME ["/certs", "/overrides"]

View File

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

View File

@@ -66,6 +66,9 @@ http {
listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %};
{% endif %} {% endif %}
{% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %} {% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %}
location ^~ /.well-known/acme-challenge/testing {
return 204;
}
location ^~ /.well-known/acme-challenge/ { location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008; proxy_pass http://127.0.0.1:8008;
} }
@@ -95,6 +98,7 @@ http {
set $webdav {{ WEBDAV_ADDRESS }}:5232; set $webdav {{ WEBDAV_ADDRESS }}:5232;
{% endif %} {% endif %}
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }}; client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
http2 on;
# Listen on HTTP only in kubernetes or behind reverse proxy # Listen on HTTP only in kubernetes or behind reverse proxy
{% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %}
@@ -109,7 +113,6 @@ http {
listen 443 ssl{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %}; listen 443 ssl{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %};
{% if SUBNET6 %} {% if SUBNET6 %}
listen [::]:443 ssl{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %}; listen [::]:443 ssl{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %};
http2 on;
{% endif %} {% endif %}
include /etc/nginx/tls.conf; include /etc/nginx/tls.conf;
@@ -159,6 +162,9 @@ http {
} }
{% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %} {% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %}
location ^~ /.well-known/acme-challenge/testing {
return 204;
}
location ^~ /.well-known/acme-challenge/ { location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008; 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] resolver = content[content.index("nameserver") + 1]
args["RESOLVER"] = f"[{resolver}]" if ":" in resolver else resolver 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 # TLS configuration
cert_name = args.get("TLS_CERT_FILENAME", "cert.pem") cert_name = args.get("TLS_CERT_FILENAME", "cert.pem")
keypair_name = args.get("TLS_KEYPAIR_FILENAME", "key.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; 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) function auth_passdb_lookup(req)
local auth_request = http_client:request { local auth_request = http_client:request {
url = "http://{{ ADMIN_ADDRESS }}:8080/internal/auth/email"; url = "http://{{ ADMIN_ADDRESS }}:8080/internal/auth/email";
} }
auth_request:add_header('Auth-Port', req.local_port) 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 if req.password ~= nil
then then
auth_request:add_header('Auth-Pass', req.password) local password = urlEncode(req.password)
auth_request:add_header('Auth-Pass', password)
end end
auth_request:add_header('Auth-Protocol', req.service) 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('Client-Port', req.remote_port)
auth_request:add_header('Auth-SSL', req.secured) auth_request:add_header('Auth-SSL', req.secured)
auth_request:add_header('Auth-Method', req.mechanism) auth_request:add_header('Auth-Method', req.mechanism)

View File

@@ -87,6 +87,11 @@ service managesieve-login {
inet_listener sieve-webmail { inet_listener sieve-webmail {
port = 14190 port = 14190
} }
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
} }
{% endif %} {% endif %}
@@ -114,6 +119,11 @@ service imap-login {
inet_listener imap-webmail { inet_listener imap-webmail {
port = 10143 port = 10143
} }
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
} }
service pop3-login { service pop3-login {
@@ -132,6 +142,11 @@ service pop3-login {
{% endif %} {% endif %}
} }
{% endif %} {% endif %}
service_count = 0
client_limit = 25000
process_min_avail = {{ CPU_COUNT }}
process_limit = {{ CPU_COUNT }}
vsz_limit = 256M
} }
recipient_delimiter = {{ RECIPIENT_DELIMITER }} recipient_delimiter = {{ RECIPIENT_DELIMITER }}
@@ -161,4 +176,11 @@ service submission-login {
inet_listener submission-webmail { inet_listener submission-webmail {
port = 10025 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 sys
import subprocess import subprocess
import time import time
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
log.basicConfig(stream=sys.stderr, level="WARNING") log.basicConfig(stream=sys.stderr, level="WARNING")
hostnames = ','.join(set(host.strip() for host in os.environ['HOSTNAMES'].split(','))) hostnames = ','.join(set(host.strip() for host in os.environ['HOSTNAMES'].split(',')))
@@ -22,6 +20,7 @@ command = [
"--preferred-challenges", "http", "--http-01-port", "8008", "--preferred-challenges", "http", "--http-01-port", "8008",
"--keep-until-expiring", "--keep-until-expiring",
"--allow-subset-of-names", "--allow-subset-of-names",
"--key-type", "rsa",
"--renew-with-new-domains", "--renew-with-new-domains",
"--config-dir", "/certs/letsencrypt", "--config-dir", "/certs/letsencrypt",
"--post-hook", "/config.py" "--post-hook", "/config.py"
@@ -45,33 +44,21 @@ command2 = [
# Wait for nginx to start # Wait for nginx to start
time.sleep(5) 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 # Run certbot every day
while True: while True:
while True: while True:
hostname = os.environ['HOSTNAMES'].split(',')[0] hostname = os.environ['HOSTNAMES'].split(',')[0]
target = f'http://{hostname}/.well-known/acme-challenge/testing' target = f'http://{hostname}/.well-known/acme-challenge/testing'
thread = Thread(target=serve_one_request) try:
thread.start()
r = requests.get(target) r = requests.get(target)
if r.status_code != 204: if r.status_code != 204:
log.critical(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.") log.critical(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.")
time.sleep(5) time.sleep(5)
else: else:
break break
thread.join() except Exception as e:
log.error(f"Exception while fetching {target}!", exc_info = e)
time.sleep(15)
subprocess.call(command) subprocess.call(command)
subprocess.call(command2) subprocess.call(command2)

View File

@@ -4,7 +4,9 @@ import os
import subprocess import subprocess
from socrate import system 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 # Check if a stale pid file exists
if os.path.exists("/var/run/nginx.pid"): if os.path.exists("/var/run/nginx.pid"):

View File

@@ -11,10 +11,10 @@ from podop import run_server
from socrate import system, conf from socrate import system, conf
system.set_env(log_filters=[ system.set_env(log_filters=[
r'(dis)?connect from localhost\[(\:\:1|127\.0\.0\.1)\]( quit=1 commands=1)?$', rb'(dis)?connect from localhost\[(\:\:1|127\.0\.0\.1)\]( quit=1 commands=1)?$',
r'haproxy read\: short protocol header\: QUIT$', rb'haproxy read\: short protocol header\: QUIT$',
r'discarding EHLO keywords\: PIPELINING$' rb'discarding EHLO keywords\: PIPELINING$'
]) ])
os.system("flock -n /queue/pid/master.pid rm /queue/pid/master.pid") 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 RUN echo $VERSION >/version
#EXPOSE 11332/tcp 11334/tcp 11335/tcp #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"] VOLUME ["/var/lib/rspamd"]

View File

@@ -9,7 +9,7 @@ COPY . /docs
RUN set -euxo pipefail \ RUN set -euxo pipefail \
; machine="$(uname -m)" \ ; machine="$(uname -m)" \
; deps="gcc musl-dev" \ ; deps="gcc musl-dev graphviz" \
; [[ "${machine}" != x86_64 ]] && \ ; [[ "${machine}" != x86_64 ]] && \
deps="${deps} cargo" \ deps="${deps} cargo" \
; apk add --no-cache --virtual .build-deps ${deps} \ ; apk add --no-cache --virtual .build-deps ${deps} \
@@ -17,7 +17,8 @@ RUN set -euxo pipefail \
mkdir -p /root/.cargo/registry/index && \ 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 \ git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \
; pip3 install -r /requirements.txt \ ; 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 \ ; sphinx-build -W /docs /build/$VERSION \
; apk del .build-deps \ ; apk del .build-deps \
; rm -rf /root/.cargo ; 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) * 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". 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. 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. 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: 5. Check if the map is available. In rspamd webgui go to configuration, a map is available with the path:
/etc/rspamd/override.d/blacklist.inc Senders domain part is on the local blacklist /overrides/blacklist.inc Senders domain part is on the local blacklist
.. image:: assets/screenshots/RspamdMapBlacklist.png .. 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. 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 - For both ``postfix.cf`` and ``postfix.master``, you need to put one configuration per line, as they are fed line-by-line
to postfix. to postfix.
- ``logrotate.conf`` as ``$ROOT/overrides/postfix/logrotate.conf`` - Replaces the logrotate.conf file used for rotating ``POSTFIX_LOG_FILE``. - ``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; - `Dovecot`_ - ``dovecot.conf`` in dovecot sub-directory.
- `Nginx`_ - All ``*.conf`` files in the ``nginx`` 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. - `Rspamd`_ - All files in the ``rspamd`` sub-directory.
- `Roundcube`_ - All ``*.inc.php`` files in the ``roundcube`` 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/database
contributors/memo contributors/memo
contributors/localization 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 #!/bin/sh
cp /etc/letsencrypt/live/domain.com/privkey.pem /mailu/certs/key.pem || exit 1 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 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: And the certbot command you will use in crontab would look something like:

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import binascii
import time import time
import os import os
from pathlib import Path from pathlib import Path
@@ -32,6 +33,19 @@ poll "{host}" proto {protocol} port {port}
{lmtp} {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): def escape_rc_string(arg):
return "".join("\\x%2x" % ord(char) for char in 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 = "options antispam 501, 504, 550, 553, 554"
options += " ssl" if fetch["tls"] else "" options += " ssl" if fetch["tls"] else ""
options += " keep" if fetch["keep"] else " fetchall" 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( fetchmailrc += RC_LINE.format(
user_email=escape_rc_string(fetch["user_email"]), user_email=escape_rc_string(fetch["user_email"]),
protocol=fetch["protocol"], protocol=fetch["protocol"],
host=escape_rc_string(fetch["host"]), host=escape_rc_string(fetch["host"]),
port=fetch["port"], 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"]), username=escape_rc_string(fetch["username"]),
password=escape_rc_string(fetch["password"]), password=escape_rc_string(fetch["password"]),
options=options, options=options,

View File

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

View File

@@ -15,7 +15,7 @@ COPY main.py ./main.py
RUN echo $VERSION >> /version RUN echo $VERSION >> /version
EXPOSE 80/tcp 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 USER mailu
CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app

View File

@@ -217,7 +217,7 @@ services:
# Optional services # Optional services
{% if antivirus_enabled %} {% if antivirus_enabled %}
antivirus: antivirus:
image: clamav/clamav-debian:1.2.0-6 image: clamav/clamav-debian:1.2.3-45
restart: always restart: always
logging: logging:
driver: journald 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 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 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/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!" echo "User testing successful!"

View File

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

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 ; mkdir -p /run/nginx /conf
# roundcube # 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 ENV CARDDAV_URL https://github.com/mstilkerich/rcmcarddav/releases/download/v5.1.0/carddav-v5.1.0.tar.gz
RUN set -euxo pipefail \ RUN set -euxo pipefail \
@@ -54,7 +54,7 @@ COPY roundcube/config/config.inc.carddav.php /var/www/roundcube/plugins/carddav/
# snappymail # 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 \ RUN set -euxo pipefail \
; mkdir /var/www/snappymail \ ; mkdir /var/www/snappymail \
@@ -94,6 +94,6 @@ VOLUME /overrides
CMD /start.py 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 RUN echo $VERSION >> /version

View File

@@ -55,6 +55,16 @@ server {
{% else %} {% else %}
fastcgi_param SCRIPT_NAME {{WEB_WEBMAIL}}/$fastcgi_script_name; fastcgi_param SCRIPT_NAME {{WEB_WEBMAIL}}/$fastcgi_script_name;
{% endif %} {% 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 ~ (^|/)\. { 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(); sp.disable_function.function("chmod").param("permissions").value("511").drop();
# Prevent various `mail`-related vulnerabilities # 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_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 # Since it's now burned, me might as well mitigate it publicly
sp.disable_function.function("putenv").param("assignment").value_r("LD_").drop() 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("array").value_r("^_").drop()
sp.disable_function.function("extract").param("flags").value("0").drop() sp.disable_function.function("extract").param("flags").value("0").drop()
# This is also burned: # See https://dustri.org/b/ini_set-based-open_basedir-bypass.html
# ini_set('open_basedir','..');chdir('..');…;chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/etc/passwd'));
# Since we have no way of matching on two parameters at the same time, we're # 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. # 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 ;) # 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("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."); 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 # 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("\\.ph").drop();
sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ht").drop(); sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ht").drop();
# Logging lockdown # 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("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.disable_function.function("ini_set").param("option").value_r("display_errors").drop()
sp.auto_cookie_secure.enable(); sp.auto_cookie_secure.enable();