{% endcall %}
{% endblock %}
diff --git a/core/admin/mailu/ui/templates/user/reply.html b/core/admin/mailu/ui/templates/user/reply.html
index 7a6b7fa8..74c604fa 100644
--- a/core/admin/mailu/ui/templates/user/reply.html
+++ b/core/admin/mailu/ui/templates/user/reply.html
@@ -14,7 +14,7 @@
{{ form.hidden_tag() }}
{{ macros.form_field(form.reply_enabled,
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')}
- else{$('#reply_subject,#reply_body,#reply_enddate').attr('readonly', '')}") }}
+ else{$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').attr('readonly', '')}") }}
{{ macros.form_field(form.reply_subject,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_body, rows=10,
diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py
index 625c02e1..eb5490bc 100644
--- a/core/admin/mailu/ui/views/base.py
+++ b/core/admin/mailu/ui/views/base.py
@@ -1,6 +1,7 @@
from mailu import models
from mailu.ui import ui, forms, access
+from flask import current_app as app
import flask
import flask_login
@@ -49,6 +50,9 @@ def announcement():
flask.flash('Your announcement was sent', 'success')
return flask.render_template('announcement.html', form=form)
+@ui.route('/webmail', methods=['GET'])
+def webmail():
+ return flask.redirect(app.config['WEB_WEBMAIL'])
@ui.route('/client', methods=['GET'])
def client():
diff --git a/core/admin/mailu/ui/views/domains.py b/core/admin/mailu/ui/views/domains.py
index 719d3844..f394ce7d 100644
--- a/core/admin/mailu/ui/views/domains.py
+++ b/core/admin/mailu/ui/views/domains.py
@@ -74,6 +74,8 @@ def domain_details(domain_name):
def domain_genkeys(domain_name):
domain = models.Domain.query.get(domain_name) or flask.abort(404)
domain.generate_dkim_key()
+ models.db.session.add(domain)
+ models.db.session.commit()
return flask.redirect(
flask.url_for(".domain_details", domain_name=domain_name))
diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py
index 9d88b54a..b1279d9e 100644
--- a/core/admin/mailu/utils.py
+++ b/core/admin/mailu/utils.py
@@ -1,11 +1,28 @@
-from mailu import models, limiter
+""" Mailu admin app utilities
+"""
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+import hmac
+import secrets
+import time
+
+from multiprocessing import Value
+
+from mailu import limiter
import flask
import flask_login
-import flask_script
import flask_migrate
import flask_babel
+import redis
+from flask.sessions import SessionMixin, SessionInterface
+from itsdangerous.encoding import want_bytes
+from werkzeug.datastructures import CallbackDict
from werkzeug.contrib import fixers
@@ -15,6 +32,7 @@ login.login_view = "ui.login"
@login.unauthorized_handler
def handle_needs_login():
+ """ redirect unauthorized requests to login page """
return flask.redirect(
flask.url_for('ui.login', next=flask.request.endpoint)
)
@@ -27,6 +45,7 @@ babel = flask_babel.Babel()
@babel.localeselector
def get_locale():
+ """ selects locale for translation """
translations = list(map(str, babel.list_translations()))
flask.session['available_languages'] = translations
@@ -41,6 +60,10 @@ def get_locale():
# Proxy fixer
class PrefixMiddleware(object):
+ """ fix proxy headers """
+ def __init__(self):
+ self.app = None
+
def __call__(self, environ, start_response):
prefix = environ.get('HTTP_X_FORWARDED_PREFIX', '')
if prefix:
@@ -56,3 +79,384 @@ proxy = PrefixMiddleware()
# Data migrate
migrate = flask_migrate.Migrate()
+
+
+# session store (inspired by https://github.com/mbr/flask-kvsession)
+class RedisStore:
+ """ Stores session data in a redis db. """
+
+ has_ttl = True
+
+ def __init__(self, redisstore):
+ self.redis = redisstore
+
+ def get(self, key):
+ """ load item from store. """
+ value = self.redis.get(key)
+ if value is None:
+ raise KeyError(key)
+ return value
+
+ def put(self, key, value, ttl=None):
+ """ save item to store. """
+ if ttl:
+ self.redis.setex(key, int(ttl), value)
+ else:
+ self.redis.set(key, value)
+
+ def delete(self, key):
+ """ delete item from store. """
+ self.redis.delete(key)
+
+ def list(self, prefix=None):
+ """ return list of keys starting with prefix """
+ if prefix:
+ prefix += b'*'
+ return list(self.redis.scan_iter(match=prefix))
+
+class DictStore:
+ """ Stores session data in a python dict. """
+
+ has_ttl = False
+
+ def __init__(self):
+ self.dict = {}
+
+ def get(self, key):
+ """ load item from store. """
+ return self.dict[key]
+
+ def put(self, key, value, ttl_secs=None):
+ """ save item to store. """
+ self.dict[key] = value
+
+ def delete(self, key):
+ """ delete item from store. """
+ try:
+ del self.dict[key]
+ except KeyError:
+ pass
+
+ def list(self, prefix=None):
+ """ return list of keys starting with prefix """
+ if prefix is None:
+ return list(self.dict.keys())
+ return [key for key in self.dict if key.startswith(prefix)]
+
+class MailuSession(CallbackDict, SessionMixin):
+ """ Custom flask session storage. """
+
+ # default modified to false
+ modified = False
+
+ def __init__(self, key=None, app=None):
+
+ self.app = app or flask.current_app
+
+ initial = None
+
+ key = want_bytes(key)
+ if parsed := self.app.session_config.parse_key(key, self.app):
+ try:
+ initial = pickle.loads(app.session_store.get(key))
+ except (KeyError, EOFError, pickle.UnpicklingError):
+ # either the cookie was manipulated or we did not find the
+ # session in the backend or the pickled data is invalid.
+ # => start new session
+ pass
+ else:
+ (self._uid, self._sid, self._created) = parsed
+ self._key = key
+
+ if initial is None:
+ # start new session
+ self.new = True
+ self._uid = None
+ self._sid = None
+ self._created = self.app.session_config.gen_created()
+ self._key = None
+
+ def _on_update(obj):
+ obj.modified = True
+
+ CallbackDict.__init__(self, initial, _on_update)
+
+ @property
+ def saved(self):
+ """ this reflects if the session was saved. """
+ return self._key is not None
+
+ @property
+ def sid(self):
+ """ this reflects the session's id. """
+ if self._sid is None or self._uid is None or self._created is None:
+ return None
+ return b''.join([self._uid, self._sid, self._created])
+
+ def destroy(self):
+ """ destroy session for security reasons. """
+
+ self.delete()
+
+ self._uid = None
+ self._sid = None
+ self._created = None
+
+ self.clear()
+
+ self.modified = True
+ self.new = False
+
+ def regenerate(self):
+ """ generate new id for session to avoid `session fixation`. """
+
+ self.delete()
+
+ self._sid = None
+ self._created = self.app.session_config.gen_created()
+
+ self.modified = True
+
+ def delete(self):
+ """ Delete stored session. """
+ if self.saved:
+ self.app.session_store.delete(self._key)
+ self._key = None
+
+ def save(self):
+ """ Save session to store. """
+
+ set_cookie = False
+
+ # set uid from dict data
+ if self._uid is None:
+ self._uid = self.app.session_config.gen_uid(self.get('user_id', ''))
+
+ # create new session id for new or regenerated sessions and force setting the cookie
+ if self._sid is None:
+ self._sid = self.app.session_config.gen_sid()
+ set_cookie = True
+
+ # get new session key
+ key = self.sid
+
+ # delete old session if key has changed
+ if key != self._key:
+ self.delete()
+
+ # remember time to refresh
+ self['_refresh'] = int(time.time()) + self.app.permanent_session_lifetime.total_seconds()/2
+
+ # save session
+ self.app.session_store.put(
+ key,
+ pickle.dumps(dict(self)),
+ self.app.permanent_session_lifetime.total_seconds()
+ )
+
+ self._key = key
+
+ self.new = False
+ self.modified = False
+
+ return set_cookie
+
+ def needs_refresh(self):
+ """ Checks if server side session needs to be refreshed. """
+
+ return int(time.time()) > self.get('_refresh', 0)
+
+class MailuSessionConfig:
+ """ Stores sessions crypto config """
+
+ # default size of session key parts
+ uid_bits = 64 # default if SESSION_KEY_BITS is not set in config
+ sid_bits = 128 # for now. must be multiple of 8!
+ time_bits = 32 # for now. must be multiple of 8!
+
+ def __init__(self, app=None):
+
+ if app is None:
+ app = flask.current_app
+
+ bits = app.config.get('SESSION_KEY_BITS', self.uid_bits)
+ if not 64 <= bits <= 256:
+ raise ValueError('SESSION_KEY_BITS must be between 64 and 256!')
+
+ uid_bytes = bits//8 + (bits%8>0)
+ sid_bytes = self.sid_bits//8
+
+ key = want_bytes(app.secret_key)
+
+ self._hmac = hmac.new(hmac.digest(key, b'SESSION_UID_HASH', digest='sha256'), digestmod='sha256')
+ self._uid_len = uid_bytes
+ self._uid_b64 = len(self._encode(bytes(uid_bytes)))
+ self._sid_len = sid_bytes
+ self._sid_b64 = len(self._encode(bytes(sid_bytes)))
+ self._key_min = self._uid_b64 + self._sid_b64
+ self._key_max = self._key_min + len(self._encode(bytes(self.time_bits//8)))
+
+ def gen_sid(self):
+ """ Generate random session id. """
+ return self._encode(secrets.token_bytes(self._sid_len))
+
+ def gen_uid(self, uid):
+ """ Generate hashed user id part of session key. """
+ _hmac = self._hmac.copy()
+ _hmac.update(want_bytes(uid))
+ return self._encode(_hmac.digest()[:self._uid_len])
+
+ def gen_created(self, now=None):
+ """ Generate base64 representation of creation time. """
+ return self._encode(int(now or time.time()).to_bytes(8, byteorder='big').lstrip(b'\0'))
+
+ def parse_key(self, key, app=None, validate=False, now=None):
+ """ Split key into sid, uid and creation time. """
+
+ if not (isinstance(key, bytes) and self._key_min <= len(key) <= self._key_max):
+ return None
+
+ uid = key[:self._uid_b64]
+ sid = key[self._uid_b64:self._key_min]
+ crt = key[self._key_min:]
+
+ # validate if parts are decodeable
+ created = self._decode(crt)
+ if created is None or self._decode(uid) is None or self._decode(sid) is None:
+ return None
+
+ # validate creation time when requested or store does not support ttl
+ if validate or not app.session_store.has_ttl:
+ if now is None:
+ now = int(time.time())
+ created = int.from_bytes(created, byteorder='big')
+ if not created < now < created + app.permanent_session_lifetime.total_seconds():
+ return None
+
+ return (uid, sid, crt)
+
+ def _encode(self, value):
+ return secrets.base64.urlsafe_b64encode(value).rstrip(b'=')
+
+ def _decode(self, value):
+ try:
+ return secrets.base64.urlsafe_b64decode(value + b'='*(4-len(value)%4))
+ except secrets.binascii.Error:
+ return None
+
+class MailuSessionInterface(SessionInterface):
+ """ Custom flask session interface. """
+
+ def open_session(self, app, request):
+ """ Load or create session. """
+ return MailuSession(request.cookies.get(app.config['SESSION_COOKIE_NAME'], None), app)
+
+ def save_session(self, app, session, response):
+ """ Save modified session. """
+
+ # If the session is modified to be empty, remove the cookie.
+ # If the session is empty, return without setting the cookie.
+ if not session:
+ if session.modified:
+ session.delete()
+ response.delete_cookie(
+ app.session_cookie_name,
+ domain=self.get_cookie_domain(app),
+ path=self.get_cookie_path(app),
+ )
+ return
+
+ # Add a "Vary: Cookie" header if the session was accessed
+ if session.accessed:
+ response.vary.add('Cookie')
+
+ set_cookie = session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST']
+ need_refresh = session.needs_refresh()
+
+ # save modified session or refresh unmodified session
+ if session.modified or need_refresh:
+ set_cookie |= session.save()
+
+ # set cookie on refreshed permanent sessions
+ if need_refresh and session.permanent:
+ set_cookie = True
+
+ # set or update cookie if necessary
+ if set_cookie:
+ response.set_cookie(
+ app.session_cookie_name,
+ session.sid,
+ expires=self.get_expiration_time(app, session),
+ httponly=self.get_cookie_httponly(app),
+ domain=self.get_cookie_domain(app),
+ path=self.get_cookie_path(app),
+ secure=self.get_cookie_secure(app),
+ samesite=self.get_cookie_samesite(app)
+ )
+
+class MailuSessionExtension:
+ """ Server side session handling """
+
+ @staticmethod
+ def cleanup_sessions(app=None):
+ """ Remove invalid or expired sessions. """
+
+ app = app or flask.current_app
+ now = int(time.time())
+
+ count = 0
+ for key in app.session_store.list():
+ if not app.session_config.parse_key(key, app, validate=True, now=now):
+ app.session_store.delete(key)
+ count += 1
+
+ return count
+
+ @staticmethod
+ def prune_sessions(uid=None, keep=None, app=None):
+ """ Remove sessions
+ uid: remove all sessions (NONE) or sessions belonging to a specific user
+ keep: keep listed sessions
+ """
+
+ keep = keep or set()
+ app = app or flask.current_app
+
+ prefix = None if uid is None else app.session_config.gen_uid(uid)
+
+ count = 0
+ for key in app.session_store.list(prefix):
+ if key not in keep:
+ app.session_store.delete(key)
+ count += 1
+
+ return count
+
+ def init_app(self, app):
+ """ Replace session management of application. """
+
+ if app.config.get('MEMORY_SESSIONS'):
+ # in-memory session store for use in development
+ app.session_store = DictStore()
+
+ else:
+ # redis-based session store for use in production
+ app.session_store = RedisStore(
+ redis.StrictRedis().from_url(app.config['SESSION_STORAGE_URL'])
+ )
+
+ # clean expired sessions oonce on first use in case lifetime was changed
+ def cleaner():
+ with cleaned.get_lock():
+ if not cleaned.value:
+ cleaned.value = True
+ flask.current_app.logger.error('cleaning')
+ MailuSessionExtension.cleanup_sessions(app)
+
+ app.before_first_request(cleaner)
+
+ app.session_config = MailuSessionConfig(app)
+ app.session_interface = MailuSessionInterface()
+
+cleaned = Value('i', False)
+session = MailuSessionExtension()
diff --git a/core/admin/package.json b/core/admin/package.json
index 72e612bc..b11672c7 100644
--- a/core/admin/package.json
+++ b/core/admin/package.json
@@ -2,30 +2,31 @@
"name": "mailu",
"version": "1.0.0",
"description": "Mailu admin assets",
- "main": "assest/index.js",
+ "main": "assets/index.js",
+ "directories": {
+ "lib": "lib"
+ },
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
- "@babel/core": "^7.4.4",
- "@babel/preset-env": "^7.4.4",
+ "@babel/core": "^7.14.6",
+ "@babel/preset-env": "^7.14.7",
"admin-lte": "^3.1.0",
- "babel-loader": "^8.0.5",
+ "babel-loader": "^8.0.6",
"css-loader": "^2.1.1",
"expose-loader": "^0.7.5",
- "file-loader": "^3.0.1",
- "jQuery": "^1.7.4",
- "less": "^3.9.0",
+ "jquery": "^3.6.0",
+ "less": "^3.13.1",
"less-loader": "^5.0.0",
- "mini-css-extract-plugin": "^0.6.0",
- "node-sass": "^4.12.0",
- "popper.js": "^1.15.0",
- "sass-loader": "^7.1.0",
- "style-loader": "^0.23.1",
- "url-loader": "^1.1.2",
- "webpack": "^4.30.0",
- "webpack-cli": "^3.3.2"
+ "mini-css-extract-plugin": "^1.2.1",
+ "node-sass": "^4.13.1",
+ "sass-loader": "^7.3.1",
+ "select2": "^4.0.13",
+ "url-loader": "^2.3.0",
+ "webpack": "^4.33.0",
+ "webpack-cli": "^3.3.12"
}
}
diff --git a/core/admin/requirements-prod.txt b/core/admin/requirements-prod.txt
index 54cf9a14..88ff2981 100644
--- a/core/admin/requirements-prod.txt
+++ b/core/admin/requirements-prod.txt
@@ -5,7 +5,7 @@ bcrypt==3.1.6
blinker==1.4
cffi==1.12.3
Click==7.0
-cryptography==3.2
+cryptography==3.4.7
decorator==4.4.0
dnspython==1.16.0
dominate==2.3.5
@@ -13,9 +13,9 @@ Flask==1.0.2
Flask-Babel==0.12.2
Flask-Bootstrap==3.3.7.1
Flask-DebugToolbar==0.10.1
-Flask-KVSession==0.6.2
Flask-Limiter==1.0.1
Flask-Login==0.4.1
+flask-marshmallow==0.14.0
Flask-Migrate==2.4.0
Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.0
@@ -25,19 +25,22 @@ idna==2.8
infinity==1.4
intervals==0.8.1
itsdangerous==1.1.0
-Jinja2==2.10.1
+Jinja2==2.11.3
limits==1.3
Mako==1.0.9
MarkupSafe==1.1.1
mysqlclient==1.4.2.post1
+marshmallow==3.10.0
+marshmallow-sqlalchemy==0.24.1
passlib==1.7.4
psycopg2==2.8.2
pycparser==2.19
-pyOpenSSL==19.0.0
+Pygments==2.8.1
+pyOpenSSL==20.0.1
python-dateutil==2.8.0
python-editor==1.0.4
pytz==2019.1
-PyYAML==5.1
+PyYAML==5.4.1
redis==3.2.1
#alpine3:12 provides six==1.15.0
#six==1.12.0
diff --git a/core/admin/requirements.txt b/core/admin/requirements.txt
index abb37234..e1de3b01 100644
--- a/core/admin/requirements.txt
+++ b/core/admin/requirements.txt
@@ -3,7 +3,6 @@ Flask-Login
Flask-SQLAlchemy
Flask-bootstrap
Flask-Babel
-Flask-KVSession
Flask-migrate
Flask-script
Flask-wtf
@@ -17,6 +16,7 @@ gunicorn
tabulate
PyYAML
PyOpenSSL
+Pygments
dnspython
bcrypt
tenacity
@@ -24,3 +24,6 @@ mysqlclient
psycopg2
idna
srslib
+marshmallow
+flask-marshmallow
+marshmallow-sqlalchemy
diff --git a/core/admin/start.py b/core/admin/start.py
index 2c925e01..0eff3bbe 100755
--- a/core/admin/start.py
+++ b/core/admin/start.py
@@ -19,7 +19,8 @@ if account is not None and domain is not None and password is not None:
os.system("flask mailu admin %s %s '%s' --mode %s" % (account, domain, password, mode))
start_command="".join([
- "gunicorn -w 4 -b :80 ",
+ "gunicorn --threads ", str(os.cpu_count()),
+ " -b :80 ",
"--access-logfile - " if (log.root.level<=log.INFO) else "",
"--error-logfile - ",
"--preload ",
diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile
index e1c20eff..22145bde 100644
--- a/core/dovecot/Dockerfile
+++ b/core/dovecot/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.13
+ARG DISTRO=alpine:3.14
FROM $DISTRO as builder
WORKDIR /tmp
RUN apk add git build-base automake autoconf libtool dovecot-dev xapian-core-dev icu-dev
diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf
index 81811cdb..6b97a086 100644
--- a/core/dovecot/conf/dovecot.conf
+++ b/core/dovecot/conf/dovecot.conf
@@ -21,7 +21,10 @@ mail_access_groups = mail
maildir_stat_dirs = yes
mailbox_list_index = yes
mail_vsize_bg_after_count = 100
-mail_plugins = $mail_plugins quota quota_clone zlib{{ ' ' }}
+mail_plugins = $mail_plugins quota quota_clone{{ ' ' }}
+ {%- if COMPRESSION -%}
+ zlib{{ ' ' }}
+ {%- endif %}
{%- if (FULL_TEXT_SEARCH or '').lower() not in ['off', 'false', '0'] -%}
fts fts_xapian
{%- endif %}
@@ -50,7 +53,7 @@ plugin {
fts_autoindex_exclude = \Trash
{% endif %}
- {% if COMPRESSION in [ 'gz', 'bz2' ] %}
+ {% if COMPRESSION in [ 'gz', 'bz2', 'lz4', 'zstd' ] %}
zlib_save = {{ COMPRESSION }}
{% endif %}
diff --git a/core/nginx/Dockerfile b/core/nginx/Dockerfile
index 2bc1cfd1..1906ed31 100644
--- a/core/nginx/Dockerfile
+++ b/core/nginx/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf
index 7a212ebe..5158ca5c 100644
--- a/core/nginx/conf/nginx.conf
+++ b/core/nginx/conf/nginx.conf
@@ -117,7 +117,7 @@ http {
include /overrides/*.conf;
# Actual logic
- {% if WEB_WEBMAIL != '/' %}
+ {% if WEB_WEBMAIL != '/' and WEBROOT_REDIRECT != 'none' %}
location / {
{% if WEBROOT_REDIRECT %}
try_files $uri {{ WEBROOT_REDIRECT }};
@@ -136,9 +136,33 @@ http {
include /etc/nginx/proxy.conf;
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
proxy_pass http://$webmail;
+ {% if ADMIN == 'true' %}
+ auth_request /internal/auth/user;
+ error_page 403 @webmail_login;
}
- {% endif %}
+ location {{ WEB_WEBMAIL }}/sso.php {
+ {% if WEB_WEBMAIL != '/' %}
+ rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
+ rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
+ {% endif %}
+ include /etc/nginx/proxy.conf;
+ client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
+ auth_request /internal/auth/user;
+ auth_request_set $user $upstream_http_x_user;
+ auth_request_set $token $upstream_http_x_user_token;
+ proxy_set_header X-Remote-User $user;
+ proxy_set_header X-Remote-User-Token $token;
+ proxy_pass http://$webmail;
+ error_page 403 @webmail_login;
+ }
+
+ location @webmail_login {
+ return 302 {{ WEB_ADMIN }}/ui/login?next=ui.webmail;
+ }
+ {% else %}
+ }
+ {% endif %}{% endif %}
{% if ADMIN == 'true' %}
location {{ WEB_ADMIN }} {
return 301 {{ WEB_ADMIN }}/ui;
diff --git a/core/none/Dockerfile b/core/none/Dockerfile
index 70041dac..51b8d1c5 100644
--- a/core/none/Dockerfile
+++ b/core/none/Dockerfile
@@ -1,6 +1,6 @@
# This is an idle image to dynamically replace any component if disabled.
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
CMD sleep 1000000d
diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile
index af29bf91..062155c1 100644
--- a/core/postfix/Dockerfile
+++ b/core/postfix/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
@@ -12,7 +12,7 @@ RUN pip3 install socrate==0.2.0
RUN pip3 install "podop>0.2.5"
# Image specific layers under this line
-RUN apk add --no-cache postfix postfix-pcre cyrus-sasl-plain cyrus-sasl-login
+RUN apk add --no-cache postfix postfix-pcre cyrus-sasl-login
COPY conf /conf
COPY start.py /start.py
diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf
index 8f35f609..9cd4010e 100644
--- a/core/postfix/conf/main.cf
+++ b/core/postfix/conf/main.cf
@@ -32,7 +32,7 @@ mydestination =
relayhost = {{ RELAYHOST }}
{% if RELAYUSER %}
smtp_sasl_auth_enable = yes
-smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
+smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
{% endif %}
@@ -58,7 +58,7 @@ tls_ssl_options = NO_COMPRESSION
smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }}
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
smtp_tls_protocols =!SSLv2,!SSLv3
-smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
+smtp_tls_session_cache_database = lmdb:${data_directory}/smtp_scache
###############
# Virtual
diff --git a/core/postfix/conf/outclean_header_filter.cf b/core/postfix/conf/outclean_header_filter.cf
index 03e33ee9..7e0e92d3 100644
--- a/core/postfix/conf/outclean_header_filter.cf
+++ b/core/postfix/conf/outclean_header_filter.cf
@@ -4,7 +4,7 @@
# Remove the first line of the Received: header. Note that we cannot fully remove the Received: header
# because OpenDKIM requires that a header be present when signing outbound mail. The first line is
# where the user's home IP address would be.
-/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user (PRIMARY_HOSTNAME [PUBLIC_IP])$1
+/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user ({{OUTCLEAN}} [{{OUTCLEAN_ADDRESS}}])$1
# Remove other typically private information.
/^\s*User-Agent:/ IGNORE
diff --git a/core/postfix/conf/sasl_passwd b/core/postfix/conf/sasl_passwd
index e19d0657..1e32322a 100644
--- a/core/postfix/conf/sasl_passwd
+++ b/core/postfix/conf/sasl_passwd
@@ -1 +1,2 @@
-{{ RELAYHOST }} {{ RELAYUSER }}:{{ RELAYPASSWORD }}
\ No newline at end of file
+{{ RELAYHOST }} {{ RELAYUSER }}:{{ RELAYPASSWORD }}
+
diff --git a/core/postfix/start.py b/core/postfix/start.py
index b68303e1..e0c781b7 100755
--- a/core/postfix/start.py
+++ b/core/postfix/start.py
@@ -8,12 +8,13 @@ import logging as log
import sys
from podop import run_server
+from pwd import getpwnam
from socrate import system, conf
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop():
- os.setuid(100)
+ os.setuid(getpwnam('postfix').pw_uid)
url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/"
# TODO: Remove verbosity setting from Podop?
run_server(0, "postfix", "/tmp/podop.socket", [
@@ -36,6 +37,15 @@ os.environ["FRONT_ADDRESS"] = system.get_host_address_from_environment("FRONT",
os.environ["ADMIN_ADDRESS"] = system.get_host_address_from_environment("ADMIN", "admin")
os.environ["ANTISPAM_MILTER_ADDRESS"] = system.get_host_address_from_environment("ANTISPAM_MILTER", "antispam:11332")
os.environ["LMTP_ADDRESS"] = system.get_host_address_from_environment("LMTP", "imap:2525")
+os.environ["OUTCLEAN"] = os.environ["HOSTNAMES"].split(",")[0]
+try:
+ _to_lookup = os.environ["OUTCLEAN"]
+ # Ensure we lookup a FQDN: @see #1884
+ if not _to_lookup.endswith('.'):
+ _to_lookup += '.'
+ os.environ["OUTCLEAN_ADDRESS"] = system.resolve_hostname(_to_lookup)
+except:
+ os.environ["OUTCLEAN_ADDRESS"] = "10.10.10.10"
for postfix_file in glob.glob("/conf/*.cf"):
conf.jinja(postfix_file, os.environ, os.path.join("/etc/postfix", os.path.basename(postfix_file)))
diff --git a/core/rspamd/Dockerfile b/core/rspamd/Dockerfile
index acaf074e..6706ef14 100644
--- a/core/rspamd/Dockerfile
+++ b/core/rspamd/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
diff --git a/core/rspamd/start.py b/core/rspamd/start.py
index bde708f2..e2e72bcb 100755
--- a/core/rspamd/start.py
+++ b/core/rspamd/start.py
@@ -10,7 +10,6 @@ log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
# Actual startup script
-os.environ["FRONT_ADDRESS"] = system.get_host_address_from_environment("FRONT", "front")
os.environ["REDIS_ADDRESS"] = system.get_host_address_from_environment("REDIS", "redis")
if os.environ.get("ANTIVIRUS") == 'clamav':
diff --git a/docs/Dockerfile b/docs/Dockerfile
index 70c9c3c4..289697da 100644
--- a/docs/Dockerfile
+++ b/docs/Dockerfile
@@ -1,20 +1,28 @@
-ARG DISTRO=alpine:3.8
-FROM $DISTRO
-
-COPY requirements.txt /requirements.txt
+# Convert .rst files to .html in temporary build container
+FROM python:3.8-alpine3.14 AS build
ARG version=master
ENV VERSION=$version
-RUN apk add --no-cache nginx curl python3 \
- && pip3 install -r /requirements.txt \
- && mkdir /run/nginx
-
-COPY ./nginx.conf /etc/nginx/conf.d/default.conf
+COPY requirements.txt /requirements.txt
COPY . /docs
-RUN mkdir -p /build/$VERSION \
- && sphinx-build -W /docs /build/$VERSION
+RUN apk add --no-cache --virtual .build-deps \
+ gcc musl-dev \
+ && pip3 install -r /requirements.txt \
+ && mkdir -p /build/$VERSION \
+ && sphinx-build -W /docs /build/$VERSION \
+ && apk del .build-deps
+
+
+# Build nginx deployment image including generated html
+FROM nginx:1.21-alpine
+
+ARG version=master
+ENV VERSION=$version
+
+COPY ./nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /build/$VERSION /build/$VERSION
EXPOSE 80/tcp
diff --git a/docs/cli.rst b/docs/cli.rst
index 8e94026b..957e47e4 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -11,6 +11,8 @@ Managing users and aliases can be done from CLI using commands:
* user-import
* user-delete
* config-update
+* config-export
+* config-import
alias
-----
@@ -62,7 +64,7 @@ primary difference with simple `user` command is that password is being imported
docker-compose run --rm admin flask mailu user-import myuser example.net '$6$51ebe0cb9f1dab48effa2a0ad8660cb489b445936b9ffd812a0b8f46bca66dd549fea530ce' 'SHA512-CRYPT'
user-delete
-------------
+-----------
.. code-block:: bash
@@ -94,7 +96,7 @@ where mail-config.yml looks like:
without ``--delete-object`` option config-update will only add/update new values but will *not* remove any entries missing in provided YAML input.
Users
------
+^^^^^
following are additional parameters that could be defined for users:
@@ -113,8 +115,197 @@ following are additional parameters that could be defined for users:
* spam_threshold
Alias
------
+^^^^^
additional fields:
* wildcard
+
+config-export
+-------------
+
+The purpose of this command is to export the complete configuration in YAML or JSON format.
+
+.. code-block:: bash
+
+ $ docker-compose exec admin flask mailu config-export --help
+
+ Usage: flask mailu config-export [OPTIONS] [FILTER]...
+
+ Export configuration as YAML or JSON to stdout or file
+
+ Options:
+ -f, --full Include attributes with default value.
+ -s, --secrets Include secret attributes (dkim-key, passwords).
+ -d, --dns Include dns records.
+ -c, --color Force colorized output.
+ -o, --output-file FILENAME Save configuration to file.
+ -j, --json Export configuration in json format.
+ -?, -h, --help Show this message and exit.
+
+Only non-default attributes are exported. If you want to export all attributes use ``--full``.
+If you want to export plain-text secrets (dkim-keys, passwords) you have to add the ``--secrets`` option.
+To include dns records (mx, spf, dkim and dmarc) add the ``--dns`` option.
+
+By default all configuration objects are exported (domain, user, alias, relay). You can specify
+filters to export only some objects or attributes (try: ``user`` or ``domain.name``).
+Attributes explicitly specified in filters are automatically exported: there is no need to add ``--secrets`` or ``--full``.
+
+.. code-block:: bash
+
+ $ docker-compose exec admin flask mailu config-export --output mail-config.yml
+
+ $ docker-compose exec admin flask mailu config-export domain.dns_mx domain.dns_spf
+
+ $ docker-compose exec admin flask mailu config-export user.spam_threshold
+
+config-import
+-------------
+
+This command imports configuration data from an external YAML or JSON source.
+
+.. code-block:: bash
+
+ $ docker-compose exec admin flask mailu config-import --help
+
+ Usage: flask mailu config-import [OPTIONS] [FILENAME|-]
+
+ Import configuration as YAML or JSON from stdin or file
+
+ Options:
+ -v, --verbose Increase verbosity.
+ -s, --secrets Show secret attributes in messages.
+ -q, --quiet Quiet mode - only show errors.
+ -c, --color Force colorized output.
+ -u, --update Update mode - merge input with existing config.
+ -n, --dry-run Perform a trial run with no changes made.
+ -?, -h, --help Show this message and exit.
+
+The current version of docker-compose exec does not pass stdin correctly, so you have to user docker exec instead:
+
+.. code-block:: bash
+
+ docker exec -i $(docker-compose ps -q admin) flask mailu config-import -nv < mail-config.yml
+
+mail-config.yml contains the configuration and looks like this:
+
+.. code-block:: yaml
+
+ domain:
+ - name: example.com
+ alternatives:
+ - alternative.example.com
+
+ user:
+ - email: foo@example.com
+ password_hash: '$2b$12$...'
+ hash_scheme: MD5-CRYPT
+
+ alias:
+ - email: alias1@example.com
+ destination:
+ - user1@example.com
+ - user2@example.com
+
+ relay:
+ - name: relay.example.com
+ comment: test
+ smtp: mx.example.com
+
+config-import shows the number of created/modified/deleted objects after import.
+To suppress all messages except error messages use ``--quiet``.
+By adding the ``--verbose`` switch the import gets more detailed and shows exactly what attributes changed.
+In all log messages plain-text secrets (dkim-keys, passwords) are hidden by default. Use ``--secrets`` to log secrets.
+If you want to test what would be done when importing without committing any changes, use ``--dry-run``.
+
+By default config-import replaces the whole configuration. ``--update`` allows to modify the existing configuration instead.
+New elements will be added and existing elements will be modified.
+It is possible to delete a single element or prune all elements from lists and associative arrays using a special notation:
+
++-----------------------------+------------------+--------------------------+
+| Delete what? | notation | example |
++=============================+==================+==========================+
+| specific array object | ``- -key: id`` | ``- -name: example.com`` |
++-----------------------------+------------------+--------------------------+
+| specific list item | ``- -id`` | ``- -user1@example.com`` |
++-----------------------------+------------------+--------------------------+
+| all remaining array objects | ``- -key: null`` | ``- -email: null`` |
++-----------------------------+------------------+--------------------------+
+| all remaining list items | ``- -prune-`` | ``- -prune-`` |
++-----------------------------+------------------+--------------------------+
+
+The ``-key: null`` notation can also be used to reset an attribute to its default.
+To reset *spam_threshold* to it's default *80* use ``-spam_threshold: null``.
+
+A new dkim key can be generated when adding or modifying a domain, by using the special value
+``dkim_key: -generate-``.
+
+This is a complete YAML template with all additional parameters that can be defined:
+
+.. code-block:: yaml
+
+ domain:
+ - name: example.com
+ alternatives:
+ - alternative.tld
+ comment: ''
+ dkim_key: ''
+ max_aliases: -1
+ max_quota_bytes: 0
+ max_users: -1
+ signup_enabled: false
+
+ user:
+ - email: postmaster@example.com
+ comment: ''
+ displayed_name: 'Postmaster'
+ enable_imap: true
+ enable_pop: false
+ enabled: true
+ fetches:
+ - id: 1
+ comment: 'test fetch'
+ error: null
+ host: other.example.com
+ keep: true
+ last_check: '2020-12-29T17:09:48.200179'
+ password: 'secret'
+ hash_password: true
+ port: 993
+ protocol: imap
+ tls: true
+ username: fetch-user
+ forward_destination:
+ - address@remote.example.com
+ forward_enabled: true
+ forward_keep: true
+ global_admin: true
+ manager_of:
+ - example.com
+ password: '$2b$12$...'
+ hash_password: true
+ quota_bytes: 1000000000
+ reply_body: ''
+ reply_enabled: false
+ reply_enddate: '2999-12-31'
+ reply_startdate: '1900-01-01'
+ reply_subject: ''
+ spam_enabled: true
+ spam_threshold: 80
+ tokens:
+ - id: 1
+ comment: email-client
+ ip: 192.168.1.1
+ password: '$5$rounds=1$...'
+
+ aliases:
+ - email: email@example.com
+ comment: ''
+ destination:
+ - address@example.com
+ wildcard: false
+
+ relay:
+ - name: relay.example.com
+ comment: ''
+ smtp: mx.example.com
diff --git a/docs/compose/.env b/docs/compose/.env
index 432b20b0..4df73080 100644
--- a/docs/compose/.env
+++ b/docs/compose/.env
@@ -97,7 +97,7 @@ WELCOME_SUBJECT=Welcome to your new email account
WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/docs/conf.py b/docs/conf.py
index 6b19f967..db7008b3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -4,7 +4,7 @@
import os
-extensions = ['sphinx.ext.imgmath', 'sphinx.ext.viewcode']
+extensions = ['sphinx.ext.imgmath', 'sphinx.ext.viewcode', 'sphinx_rtd_theme']
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
@@ -36,7 +36,7 @@ html_context = {
'github_user': 'mailu',
'github_repo': 'mailu',
'github_version': version,
- 'stable_version': '1.7',
+ 'stable_version': '1.8',
'versions': [
('1.5', '/1.5/'),
('1.6', '/1.6/'),
diff --git a/docs/configuration.rst b/docs/configuration.rst
index e08675a8..21effc52 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -41,7 +41,7 @@ The ``AUTH_RATELIMIT`` holds a security setting for fighting attackers that
try to guess user passwords. The value is the limit of failed authentication attempts
that a single IP address can perform against IMAP, POP and SMTP authentication endpoints.
-If ``AUTH_RATELIMIT_SUBNET`` is ``True`` (which is the default), the ``AUTH_RATELIMIT``
+If ``AUTH_RATELIMIT_SUBNET`` is ``True`` (default: False), the ``AUTH_RATELIMIT``
rules does also apply to auth requests coming from ``SUBNET``, especially for the webmail.
If you disable this, ensure that the rate limit on the webmail is enforced in a different
way (e.g. roundcube plug-in), otherwise an attacker can simply bypass the limit using webmail.
@@ -99,14 +99,19 @@ the localpart for DMARC rua and ruf email addresses.
Full-text search is enabled for IMAP is enabled by default. This feature can be disabled
(e.g. for performance reasons) by setting the optional variable ``FULL_TEXT_SEARCH`` to ``off``.
+.. _web_settings:
+
Web settings
------------
-The ``WEB_ADMIN`` contains the path to the main admin interface, while
-``WEB_WEBMAIL`` contains the path to the Web email client.
-The ``WEBROOT_REDIRECT`` redirects all non-found queries to the set path.
-An empty ``WEBROOT_REDIRECT`` value disables redirecting and enables classic
-behavior of a 404 result when not found.
+- ``WEB_ADMIN`` contains the path to the main admin interface
+
+- ``WEB_WEBMAIL`` contains the path to the Web email client.
+
+- ``WEBROOT_REDIRECT`` redirects all non-found queries to the set path.
+ An empty ``WEBROOT_REDIRECT`` value disables redirecting and enables classic behavior of a 404 result when not found.
+ Alternatively, ``WEBROOT_REDIRECT`` can be set to ``none`` if you are using an Nginx override for ``location /``.
+
All three options need a leading slash (``/``) to work.
.. note:: ``WEBROOT_REDIRECT`` has to point to a valid path on the webserver.
@@ -195,4 +200,24 @@ resolved. This can be used to rely on DNS based service discovery with changing
When using ``*_ADDRESS``, the hostnames must be full-qualified hostnames. Otherwise nginx will not be able to
resolve the hostnames.
+Database settings
+-----------------
+
+The admin service stores configurations in a database.
+
+- ``DB_FLAVOR``: the database type for mailu admin service. (``sqlite``, ``postgresql``, ``mysql``)
+- ``DB_HOST``: the database host for mailu admin service. (when not ``sqlite``)
+- ``DB_PORT``: the database port for mailu admin service. (when not ``sqlite``)
+- ``DB_PW``: the database password for mailu admin service. (when not ``sqlite``)
+- ``DB_USER``: the database user for mailu admin service. (when not ``sqlite``)
+- ``DB_NAME``: the database name for mailu admin service. (when not ``sqlite``)
+
+The roundcube service stores configurations in a database.
+
+- ``ROUNDCUBE_DB_FLAVOR``: the database type for roundcube service. (``sqlite``, ``postgresql``, ``mysql``)
+- ``ROUNDCUBE_DB_HOST``: the database host for roundcube service. (when not ``sqlite``)
+- ``ROUNDCUBE_DB_PORT``: the database port for roundcube service. (when not ``sqlite``)
+- ``ROUNDCUBE_DB_PW``: the database password for roundcube service. (when not ``sqlite``)
+- ``ROUNDCUBE_DB_USER``: the database user for roundcube service. (when not ``sqlite``)
+- ``ROUNDCUBE_DB_NAME``: the database name for roundcube service. (when not ``sqlite``)
diff --git a/docs/contributors/environment.rst b/docs/contributors/environment.rst
index 26c04d0b..cef71c6c 100644
--- a/docs/contributors/environment.rst
+++ b/docs/contributors/environment.rst
@@ -178,9 +178,9 @@ In the case of a PR from a fellow team member, a single review is enough
to initiate merging. In all other cases, two approving reviews are required.
There is also a possibility to set the ``review/need2`` to require a second review.
-After Travis successfully tests the PR and the required amount of reviews are acquired,
+After the Github Action workflow successfully tests the PR and the required amount of reviews are acquired,
Mergify will trigger with a ``bors r+`` command. Bors will batch any approved PR's,
-merges them with master in a staging branch where Travis builds and tests the result.
+merges them with master in a staging branch where the Github Action workflow builds and tests the result.
After a successful test, the actual master gets fast-forwarded to that point.
System requirements
@@ -201,16 +201,16 @@ us on `Matrix`_.
Test images
```````````
-All PR's automatically get build by Travis, controlled by `bors-ng`_.
+All PR's automatically get build by a Github Action workflow, controlled by `bors-ng`_.
Some primitive auto testing is done.
The resulting images get uploaded to Docker hub, under the
-tag name ``mailutest/:pr-``.
+tag name ``mailuci/:pr-``.
For example, to test PR #500 against master, reviewers can use:
.. code-block:: bash
- export DOCKER_ORG="mailutest"
+ export DOCKER_ORG="mailuci"
export MAILU_VERSION="pr-500"
docker-compose pull
docker-compose up -d
@@ -232,8 +232,8 @@ after Bors confirms a successful build.
When bors try fails
```````````````````
-Sometimes Travis fails when another PR triggers a ``bors try`` command,
-before Travis cloned the git repository.
+Sometimes the Github Action workflow fails when another PR triggers a ``bors try`` command,
+before the Github Action workflow cloned the git repository.
Inspect the build log in the link provided by *bors-ng* to find out the cause.
If you see something like the following error on top of the logs,
feel free to write a comment with ``bors retry``.
diff --git a/docs/contributors/workflow.rst b/docs/contributors/workflow.rst
index 16dcef52..31ffd793 100644
--- a/docs/contributors/workflow.rst
+++ b/docs/contributors/workflow.rst
@@ -41,7 +41,7 @@ PR Workflow
-----------
All pull requests have to be against the main ``master`` branch.
-The PR gets build by Travis and some primitive auto-testing is done.
+The PR gets build by a Github Action workflow and some primitive auto-testing is done.
Test images get uploaded to a separate section in Docker hub.
Reviewers will check the PR and test the resulting images.
See the :ref:`testing` section for more info.
diff --git a/docs/database.rst b/docs/database.rst
index b2526d6f..0f8318d5 100644
--- a/docs/database.rst
+++ b/docs/database.rst
@@ -8,7 +8,8 @@ This functionality should still be considered experimental!
Mailu Postgresql
----------------
-Mailu optionally comes with a pre-configured Postgresql image.
+Mailu optionally comes with a pre-configured Postgresql image, which as of 1.8, is deprecated
+and will be removed in 1.9.
This images has the following features:
- Automatic creation of users, db, extensions and password;
diff --git a/docs/faq.rst b/docs/faq.rst
index 9c4f1d75..a2c6bd33 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -61,7 +61,7 @@ have to prevent pushing out something quickly.
We currently maintain a strict work flow:
#. Someone writes a solution and sends a pull request;
-#. We use Travis-CI for some very basic building and testing;
+#. We use Github actions for some very basic building and testing;
#. The pull request needs to be code-reviewed and tested by at least two members
from the contributors team.
@@ -261,10 +261,14 @@ correct syntax. The following file names will be taken as override configuration
- ``main.cf`` as ``$ROOT/overrides/postfix/postfix.cf``
- ``master.cf`` as ``$ROOT/overrides/postfix/postfix.master``
- All ``$ROOT/overrides/postfix/*.map`` files
+ - For both ``postfix.cf`` and ``postfix.master``, you need to put one configuration per line, as they are fed line-by-line
+ to postfix.
- `Dovecot`_ - ``dovecot.conf`` in dovecot sub-directory;
- `Nginx`_ - All ``*.conf`` files in the ``nginx`` sub-directory;
- `Rspamd`_ - All files in the ``rspamd`` sub-directory.
+To override the root location (``/``) in Nginx ``WEBROOT_REDIRECT`` needs to be set to ``none`` in the env file (see :ref:`web settings `).
+
*Issue reference:* `206`_, `1368`_.
I want to integrate Nextcloud 15 (and newer) with Mailu
@@ -495,6 +499,8 @@ follow these steps:
logging:
driver: journald
+ options:
+ tag: mailu-front
2. Add the /etc/fail2ban/filter.d/bad-auth.conf
@@ -504,6 +510,7 @@ follow these steps:
[Definition]
failregex = .* client login failed: .+ client:\
ignoreregex =
+ journalmatch = CONTAINER_TAG=mailu-front
3. Add the /etc/fail2ban/jail.d/bad-auth.conf
@@ -511,8 +518,8 @@ follow these steps:
[bad-auth]
enabled = true
+ backend = systemd
filter = bad-auth
- logpath = /var/log/messages
bantime = 604800
findtime = 300
maxretry = 10
diff --git a/docs/kubernetes/mailu/front.yaml b/docs/kubernetes/mailu/front.yaml
index a1d5acb2..2fba1026 100644
--- a/docs/kubernetes/mailu/front.yaml
+++ b/docs/kubernetes/mailu/front.yaml
@@ -1,4 +1,4 @@
-apiVersion: apps/v1beta2
+apiVersion: apps/v1
kind: DaemonSet
metadata:
name: mailu-front
diff --git a/docs/kubernetes/mailu/index.rst b/docs/kubernetes/mailu/index.rst
index 5d3502a7..0af3942e 100644
--- a/docs/kubernetes/mailu/index.rst
+++ b/docs/kubernetes/mailu/index.rst
@@ -3,6 +3,10 @@
Kubernetes setup
================
+> Hold up!
+> These instructions are not recommended for setting up Mailu in a production Kubernetes environment.
+> Please see [the Helm Chart documentation](https://github.com/Mailu/helm-charts/blob/master/mailu/README.md).
+
Prequisites
-----------
diff --git a/docs/releases.rst b/docs/releases.rst
index 7a15d1fa..6c672538 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -1,8 +1,81 @@
Release notes
=============
-Mailu 1.8 - 2020-10-02
-----------------------
+Mailu 1.8 - 2021-08-7
+---------------------
+
+The full 1.8 release is finally ready. There have been some changes in the contributors team. Many people from the contributors team have stepped back due to changed priorities in their life.
+We are very grateful for all their contributions and hope we will see them back again in the future.
+This is the main reason why it took so long for 1.8 to be fully released.
+
+Fortunately more people have decided to join the project. Some very nice contributions have been made which will become part of the next 1.9 release.
+We hope that future Mailu releases will be released more quickly now we have more active contributors again.
+
+For a list of all changes refer to `CHANGELOG.md` in the root folder of the Mailu github project. Please read the 'Override location changes' section further on this page. It contains important information for the people who use the overrides folder.
+
+New Functionality & Improvements
+````````````````````````````````
+
+Here’s a short summary of new features:
+
+- Roundcube and Rainloop have been updated.
+- All dependencies have been updated to the latest security update.
+- Fail2ban documentation has been improved.
+- Switch from client side (cookie) sessions to server side sessions and protect against session-fixation attacks. We recommend that you change your SECRET_KEY after upgrading.
+- Full-text-search is back after having been disabled for a while due to nasty bugs. It can still be disabled via the mailu.env file.
+- Tons of documentation improvements, especially geared towards new users.
+- (Experimental) support for different architectures, such as ARM.
+- Improvements around webmails, such as CardDAV, GPG and a new skin for an updated roundcube, and support for MySQL for it. Updated Rainloop, too.
+- Improvements around relaying, such as AUTH LOGIN and non-standard port support.
+- Update to alpine:3.14 as baseimage for most containers.
+- Setup warns users about compose-IPv6 deployments which have caused open relays in the past.
+- Improved handling of upper-vs-lowercase aliases and user-addresses.
+- Improved rate-limiting system.
+- Support for SRS.
+- Japanese localisation is now available.
+
+
+Upgrading
+`````````
+
+Upgrade should run fine as long as you generate a new compose or stack
+configuration and upgrade your mailu.env.
+
+Please note that the shipped image for PostgreSQL database is deprecated.
+The shipped image for PostgreSQL is not maintained anymore from release 1.8.
+We recommend switching to an external PostgreSQL image as soon as possible.
+
+Override location changes
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you have regenerated the Docker compose and environment files, there are some changes to the configuration overrides.
+Override files are now mounted read-only into the containers. The Dovecot and Postfix overrides are moved in their own sub-directory. If there are local override files, they will need to be moved from ``overrides/`` to ``overrides/dovecot`` and ``overrides/postfix/``.
+
+Recreate SECRET_KEY after upgrading
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Improvements have been made to protect again session-fixation attacks.
+To be fully protected, it is required to change your SECRET_KEY in Mailu.env after upgrading.
+A new SECRET_KEY is generated when you recreate your docker-compose.yml & mailu.env file via setup.mailu.io.
+
+The SECRET_KEY is an uppercase alphanumeric string of length 16. You can manually create such a string via
+```cat /dev/urandom | tr -dc 'A-Z0-9' | fold -w ${1:-16} | head -n 1```
+
+After changing mailu.env, it is required to recreate all containers for the changes to be propagated.
+
+Update your DNS SPF Records
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+It has become known that the SPF DNS records generated by the admin interface are not completely standard compliant anymore. Please check the DNS records for your domains and compare them to what the new admin-interface instructs you to use. In most cases, this should be a simple copy-paste operation for you ….
+
+Fixed hostname for antispam service
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For history to be retained in Rspamd, the antispam container requires a static hostname. When you re-generate your docker-compose.yml file (or helm-chart), this will be covered.
+
+
+Mailu 1.8rc - 2020-10-02
+------------------------
Release 1.8 has come a long way again. Due to corona the project slowed down to a crawl. Fortunately new contributors have joined the team what enabled us to still release Mailu 1.8 this year.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 4afd9bb6..f49e26d5 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -2,3 +2,4 @@ recommonmark
Sphinx
sphinx-autobuild
sphinx-rtd-theme
+docutils==0.16
diff --git a/docs/reverse.rst b/docs/reverse.rst
index de710dff..29f9e9e1 100644
--- a/docs/reverse.rst
+++ b/docs/reverse.rst
@@ -154,7 +154,40 @@ Add the respective Traefik labels for your domain/configuration, like
If your Traefik is configured to automatically request certificates from *letsencrypt*, then you’ll have a certificate for ``mail.your.doma.in`` now. However,
``mail.your.doma.in`` might only be the location where you want the Mailu web-interfaces to live — your mail should be sent/received from ``your.doma.in``,
and this is the ``DOMAIN`` in your ``.env``?
-To support that use-case, Traefik can request ``SANs`` for your domain. Lets add something like
+To support that use-case, Traefik can request ``SANs`` for your domain. The configuration for this will depend on your Traefik version.
+
+----
+
+Traefik 2.x using labels configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add the appropriate labels for your domain(s) to the ``front`` container in ``docker-compose.yml``.
+
+.. code-block:: yaml
+
+ services:
+ front:
+ labels:
+ # Enable TLS
+ - "traefik.http.routers.mailu-secure.tls"
+ # Your main domain
+ - "traefik.http.routers.mailu-secure.tls.domains[0].main=your.doma.in"
+ # Optional SANs for your main domain
+ - "traefik.http.routers.mailu-secure.tls.domains[0].sans=mail.your.doma.in,webmail.your.doma.in,smtp.your.doma.in"
+ # Optionally add other domains
+ - "traefik.http.routers.mailu-secure.tls.domains[1].main=mail.other.doma.in"
+ - "traefik.http.routers.mailu-secure.tls.domains[1].sans=mail2.other.doma.in,mail3.other.doma.in"
+ # Your ACME certificate resolver
+ - "traefik.http.routers.mailu-secure.tls.certResolver=foo"
+
+Of course, be sure to define the Certificate Resolver ``foo`` in the static configuration as well.
+
+Alternatively, you can define SANs in the Traefik static configuration using routers, or in the static configuration using entrypoints. Refer to the Traefik documentation for more details.
+
+Traefik 1.x with TOML configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Lets add something like
.. code-block:: yaml
@@ -163,7 +196,11 @@ To support that use-case, Traefik can request ``SANs`` for your domain. Lets add
main = "your.doma.in" # this is the same as $TRAEFIK_DOMAIN!
sans = ["mail.your.doma.in", "webmail.your.doma.in", "smtp.your.doma.in"]
-to your ``traefik.toml``. You might need to clear your ``acme.json``, if a certificate for one of these domains already exists.
+to your ``traefik.toml``.
+
+----
+
+You might need to clear your ``acme.json``, if a certificate for one of these domains already exists.
You will need some solution which dumps the certificates in ``acme.json``, so you can include them in the ``mailu/front`` container.
One such example is ``mailu/traefik-certdumper``, which has been adapted for use in Mailu. You can add it to your ``docker-compose.yml`` like:
diff --git a/docs/webadministration.rst b/docs/webadministration.rst
index 070eb473..86ce41c0 100644
--- a/docs/webadministration.rst
+++ b/docs/webadministration.rst
@@ -1,7 +1,7 @@
Web administration interface
============================
-The web administration interface is the main website for maintaining your Mailu installation.
+The web administration interface is the main website for maintaining your Mailu installation.
For brevity the web administration interface will now be mentioned as admin gui.
It offers the following configuration options:
@@ -30,13 +30,13 @@ It offers the following configuration options:
* Configure all email domains served by Mailu, including:
* generating dkim and dmarc keys for a domain.
-
+
* view email domain information on how to configure your SPF, DMARC, DKIM and MX dns records for an email domain.
-
+
* Add new email domains.
-
+
* For existing domains, configure users, quotas, aliases, administrators and alternative domain names.
-
+
* access the webmail site.
* lookup settings for configuring your email client.
@@ -49,7 +49,7 @@ The admin GUI is by default accessed via the URL `https:///admin`, wh
To login the admin GUI enter the email address and password of an user.
Only global administrator users have access to all configuration settings and the Rspamd webgui. Other users will be presented with settings for only their account, and domains they are managers of.
-To create a user who is a global administrator for a new installation, the Mailu.env file can be adapted.
+To create a user who is a global administrator for a new installation, the Mailu.env file can be adapted.
For more information see the section 'Admin account - automatic creation' in :ref:`the configuration reference `.
The following sections are only accessible for global administrators:
@@ -69,7 +69,7 @@ The following sections are only accessible for global administrators:
Settings
--------
-After logging in the web administration interface, the settings page is loaded.
+After logging in the web administration interface, the settings page is loaded.
On the settings page the settings of the currently logged in user can be changed.
Changes are saved and effective immediately after clicking the Save Settings button at the bottom of the page.
@@ -77,27 +77,27 @@ Changes are saved and effective immediately after clicking the Save Settings but
Display name
````````````
-On the settings page the displayed name can be changed of the logged in user.
+On the settings page the displayed name can be changed of the logged in user.
This display name is only used within the web administration interface.
Antispam
````````
-Under the section `Antispam` the spam filter can be enabled or disabled for the logged in user. By default the spam filter is enabled.
+Under the section `Antispam` the spam filter can be enabled or disabled for the logged in user. By default the spam filter is enabled.
When the spam filter is disabled, all received email messages will go to the inbox folder of the logged in user.
The exception to this rule, are email messages with an extremely high spam score. These email messages are always rejected by Rspamd.
When the spam filter is enabled, received email messages will be moved to the logged in user's inbox folder or junk folder depending on the user defined spam filter tolerance.
-The user defined spam filter tolerance determines when an email is classified as ham (moved to the inbox folder) or spam (moved to the junk folder).
-The default value is 80%. The lower the spam filter tolerance, the more false positives (ham classified as spam). The higher the spam filter tolerance, the more false negatives (spam classified as ham).
+The user defined spam filter tolerance determines when an email is classified as ham (moved to the inbox folder) or spam (moved to the junk folder).
+The default value is 80%. The lower the spam filter tolerance, the more false positives (ham classified as spam). The higher the spam filter tolerance, the more false negatives (spam classified as ham).
For more information see the :ref:`antispam documentation `.
Auto-forward
`````````````
-Under the section `Auto-forward`, the automatic forwarding of received email messages can be enabled. When enabled, all received email messages are forwarded to the specified email address.
+Under the section `Auto-forward`, the automatic forwarding of received email messages can be enabled. When enabled, all received email messages are forwarded to the specified email address.
The option "Keep a copy of the emails" can be ticked, to keep a copy of the received email message in the inbox folder.
@@ -107,7 +107,7 @@ In the destination textbox, the email addresses can be entered for automatic for
Update password
---------------
-On the `update password` page, the password of the logged in user can be changed. Changes are effective immediately.
+On the `update password` page, the password of the logged in user can be changed. Changes are effective immediately.
.. _webadministration_auto-reply:
@@ -117,7 +117,7 @@ Auto-reply
On the `auto-reply` page, automatic replies can be configured. This is also known as out of office (ooo) or out of facility (oof) replies.
-To enable automatic replies tick the checkbox 'Enable automatic reply'.
+To enable automatic replies tick the checkbox 'Enable automatic reply'.
Under Reply subject the email subject for automatic replies can be configured. When a reply subject is entered, this subject will be used for the automatic reply.
@@ -130,12 +130,12 @@ E.g. if the email subject of the received email message is "how are you?", then
Fetched accounts
----------------
-This page is only available when the Fetchmail container is part of your Mailu deployment.
+This page is only available when the Fetchmail container is part of your Mailu deployment.
Fetchmail can be enabled when creating the docker-compose.yml file with the setup utility (https://setup.mailu.io).
On the `fetched accounts` page you can configure email accounts from which email messages will be retrieved.
-Only unread email messages are retrieved from the specified email account.
-By default Fetchmail will retrieve email messages every 10 minutes. This can be changed in the Mailu.env file.
+Only unread email messages are retrieved from the specified email account.
+By default Fetchmail will retrieve email messages every 10 minutes. This can be changed in the Mailu.env file.
For more information on changing the polling interval see :ref:`the configuration reference `.
@@ -149,7 +149,7 @@ You can add a fetched account by clicking on the `Add an account` button on the
* Enable TLS. Tick this setting if the email server requires TLS/SSL instead of STARTTLS.
-* Username. The user name for logging in to the email server. Normally this is the email address or the email address' local-part (the part before @).
+* Username. The user name for logging in to the email server. Normally this is the email address or the email address' local-part (the part before @).
* Password. The password for logging in to the email server.
@@ -166,8 +166,8 @@ The purpose of an authentication token is to create a unique and strong password
The application will use this authentication token instead of the logged in user's password for sending/receiving email.
This allows safe access to the logged in user's email account. At any moment, the authentication token can be deleted so that the application has no access to the logged in user's email account anymore.
-By clicking on the New token button on the top right of the page, a new authentication token can be created. On this page the generated authentication token will only be displayed once.
-After saving the application token it is not possible anymore to view the unique password.
+By clicking on the New token button on the top right of the page, a new authentication token can be created. On this page the generated authentication token will only be displayed once.
+After saving the application token it is not possible anymore to view the unique password.
The comment field can be used to enter a description for the authentication token. For example the name of the application the application token is created for.
@@ -198,9 +198,9 @@ A global administrator can change `any setting` in the admin GUI. Be careful tha
Relayed domains
---------------
-On the `relayed domains list` page, destination domains can be added that Mailu will relay email messages for without authentication.
-This means that for these destination domains, other email clients or email servers can send email via Mailu unauthenticated via port 25 to this destination domain.
-For example if the destination domain example.com is added. Any emails to example.com (john@example.com) will be relayed to example.com.
+On the `relayed domains list` page, destination domains can be added that Mailu will relay email messages for without authentication.
+This means that for these destination domains, other email clients or email servers can send email via Mailu unauthenticated via port 25 to this destination domain.
+For example if the destination domain example.com is added. Any emails to example.com (john@example.com) will be relayed to example.com.
Example scenario's are:
* relay domain from a backup server.
@@ -212,30 +212,37 @@ Example scenario's are:
On the new relayed domain page the following options can be entered for a new relayed domain:
-* Relayed domain name. The domain name that is relayed. Email messages addressed to this domain (To: John@example.com), will be forwarded to this domain.
- No authentication is required.
+* Relayed domain name. The domain name that is relayed. Email messages addressed to this domain (To: John@example.com), will be forwarded to this domain.
+ No authentication is required.
-* Remote host (optional). The SMPT server that will be used for relaying the email message.
- When this field is blank, the Mailu server will directly send the email message to the relayed domain.
- As value can be entered either a hostname or IP address of the SMPT server.
- By default port 25 is used. To use a different port append ":port number" to the Remote Host. For example:
- 123.45.67.90:2525.
+* Remote host (optional). The host that will be used for relaying the email message.
+ When this field is blank, the Mailu server will directly send the email message to the mail server of the relayed domain.
+ When a remote host is specified it can be prefixed by ``mx:`` or ``lmtp:`` and followed by a port number: ``:port``).
+
+ ================ ===================================== =========================
+ Remote host Description postfix transport:nexthop
+ ================ ===================================== =========================
+ empty use MX of relay domain smtp:domain
+ :port use MX of relay domain and use port smtp:domain:port
+ target resolve A/AAAA of target smtp:[target]
+ target:port resolve A/AAAA of target and use port smtp:[target]:port
+ mx:target resolve MX of target smtp:target
+ mx:target:port resolve MX of target and use port smtp:target:port
+ lmtp:target resolve A/AAAA of target lmtp:target
+ lmtp:target:port resolve A/AAAA of target and use port lmtp:target:port
+ ================ ===================================== =========================
+
+ `target` can also be an IPv4 or IPv6 address (an IPv6 address must be enclosed in []: ``[2001:DB8::]``).
* Comment. A text field where a comment can be entered to describe the entry.
Changes are effective immediately after clicking the Save button.
-NOTE: Due to bug `1588`_ email messages fail to be relayed if no Remote Host is configured.
-As a workaround the HOSTNAME or IP Address of the SMPT server of the relayed domain can be entered as Remote Host.
-Please note that no MX lookup is performed when entering a hostname as Remote Host. You can use the MX lookup on mxtoolbox.com to find the hostname and IP Address of the SMTP server.
-
-.. _`1588`: https://github.com/Mailu/Mailu/issues/1588
-
Antispam
--------
The menu item Antispam opens the Rspamd webgui. For more information how spam filtering works in Mailu see the :ref:`Spam filtering page `.
-The spam filtering page also contains a section that describes how to create a local blacklist for blocking email messages from specific domains.
+The spam filtering page also contains a section that describes how to create a local blacklist for blocking email messages from specific domains.
The Rspamd webgui offers basic functions for setting metric actions, scores, viewing statistics and learning.
The following settings are not persisent and are *lost* when the Antispam container is recreated or restarted:
@@ -266,31 +273,31 @@ On the `Mail domains` page all the domains served by Mailu are configured. Via t
Details
```````
-This page is also accessible for domain managers. On the details page all DNS settings are displayed for configuring your DNS server. It contains information on what to configure as MX record and SPF record. On this page it is also possible to (re-)generate the keys for DKIM and DMARC. The option for generating keys for DKIM and DMARC is only available for global administrators. After generating the keys for DKIM and DMARC, this page will also show the DNS records for configuring the DKIM/DMARC records on the DNS server.
+This page is also accessible for domain managers. On the details page all DNS settings are displayed for configuring your DNS server. It contains information on what to configure as MX record and SPF record. On this page it is also possible to (re-)generate the keys for DKIM and DMARC. The option for generating keys for DKIM and DMARC is only available for global administrators. After generating the keys for DKIM and DMARC, this page will also show the DNS records for configuring the DKIM/DMARC records on the DNS server.
Edit
-````
+````
-This page is only accessible for global administrators. On the edit page, the global settings for the domain can be changed.
+This page is only accessible for global administrators. On the edit page, the global settings for the domain can be changed.
* Maximum user count. The maximum amount of users that can be created under this domain. Once this limit is reached it is not possible anymore to add users to the domain; and it is also not possible for users to self-register.
-
+
* Maximum alias count. The maximum amount of aliases that can be created for an email account.
-
+
* Maximum user quota. The maximum amount of quota that can be assigned to a user. When creating or editing a user, this sets the limit on the maximum amount of quota that can be assigned to the user.
-
-* Enable sign-up. When this option is ticked, self-registration is enabled. When the Admin GUI is accessed, in the menu list the option Signup becomes available.
- Obviously this menu item is only visible when signed out. On the Signup page a user can create an email account.
- If your Admin GUI is available to the public internet, this means your Mailu installation basically becomes a free email provider.
+
+* Enable sign-up. When this option is ticked, self-registration is enabled. When the Admin GUI is accessed, in the menu list the option Signup becomes available.
+ Obviously this menu item is only visible when signed out. On the Signup page a user can create an email account.
+ If your Admin GUI is available to the public internet, this means your Mailu installation basically becomes a free email provider.
Use this option with care!
-
+
* Comment. Description for the domain. This description is visible on the parent domains list page.
Delete
``````
-This page is only accessible for global administrators. This page allows you to delete the domain. The Admin GUI will ask for confirmation if the domain must be really deleted.
+This page is only accessible for global administrators. This page allows you to delete the domain. The Admin GUI will ask for confirmation if the domain must be really deleted.
Users
@@ -326,7 +333,7 @@ For adding a new user the following options can be configured.
* Enabled. Tick this checkbox to enable the user account. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail.
The email inbox of the user is still retained. This option can be used to temporarily suspend an user account.
-
+
* Quota. The maximum quota for the user's email box.
* Allow IMAP access. When ticked, allows email retrieval via the IMAP protocol.
@@ -337,7 +344,7 @@ For adding a new user the following options can be configured.
Aliases
```````
-This page is also accessible for domain managers. On the aliases page, aliases can be added for email addresses. An alias is a way to disguise another email address.
+This page is also accessible for domain managers. On the aliases page, aliases can be added for email addresses. An alias is a way to disguise another email address.
Everything sent to an alias email address is actually received in the primary email account's inbox of the destination email address.
Aliases can diversify a single email account without having to create multiple email addresses (users).
It is also possible to add multiple email addresses to the destination field. All incoming mails will be sent to each users inbox in this case.
@@ -348,11 +355,11 @@ The following options are available when adding an alias:
* Use SQL LIKE Syntax (e.g. for catch-all aliases). When this option is ticked, you can use SQL LIKE syntax as alias.
The SQL LIKE syntax is used to match text values against a pattern using wildcards. There are two wildcards that can be used with SQL LIKE syntax:
-
+
* % - The percent sign represents zero, one, or multiple characters
* _ - The underscore represents a single character
-
- Examples are:
+
+ Examples are:
* a% - Finds any values that start with "a"
* %a - Finds any values that end with "a"
* %or% - Finds any values that have "or" in any position
@@ -369,7 +376,7 @@ The following options are available when adding an alias:
Managers
````````
-This page is also accessible for domain managers. On the `managers list` page, managers can be added for the domain and can be deleted.
+This page is also accessible for domain managers. On the `managers list` page, managers can be added for the domain and can be deleted.
Managers have access to configuration settings of the domain.
On the `add manager` page you can click on the manager email text box to access a drop down list of users that can be made a manager of the domain.
@@ -377,11 +384,11 @@ On the `add manager` page you can click on the manager email text box to access
Alternatives
````````````
-This page is only accessible for global administrators. On the alternatives page, alternative domains can be added for the domain.
+This page is only accessible for global administrators. On the alternatives page, alternative domains can be added for the domain.
An alternative domain acts as a copy of a given domain.
-Everything sent to an alternative domain, is actually received in the domain the alternative is created for.
-This allows you to receive emails for multiple domains while using a single domain.
-For example if the main domain has the email address user@example.com, and the alternative domain is mymail.com,
+Everything sent to an alternative domain, is actually received in the domain the alternative is created for.
+This allows you to receive emails for multiple domains while using a single domain.
+For example if the main domain has the email address user@example.com, and the alternative domain is mymail.com,
then email send to user@mymail.com will end up in the email box of user@example.com.
New domain
@@ -392,16 +399,16 @@ This page is only accessible for global administrators. Via this page a new doma
* domain name. The name of the domain.
* Maximum user count. The maximum amount of users that can be created under this domain. Once this limit is reached it is not possible anymore to add users to the domain; and it is also not possible for users to self-register.
-
+
* Maximum alias count. The maximum amount of aliases that can be made for an email account.
-
+
* Maximum user quota. The maximum amount of quota that can be assigned to a user. When creating or editing a user, this sets the limit on the maximum amount of quota that can be assigned to the user.
-
-* Enable sign-up. When this option is ticked, self-registration is enabled. When the Admin GUI is accessed, in the menu list the option Signup becomes available.
- Obviously this menu item is only visible when signed out. On the Signup page a user can create an email account.
- If your Admin GUI is available to the public internet, this means your Mailu installation basically becomes a free email provider.
+
+* Enable sign-up. When this option is ticked, self-registration is enabled. When the Admin GUI is accessed, in the menu list the option Signup becomes available.
+ Obviously this menu item is only visible when signed out. On the Signup page a user can create an email account.
+ If your Admin GUI is available to the public internet, this means your Mailu installation basically becomes a free email provider.
Use this option with care!
-
+
* Comment. Description for the domain. This description is visible on the parent domains list page.
@@ -414,7 +421,7 @@ The menu item `Webmail` opens the webmail page. This option is only available if
Client setup
------------
-The menu item `Client setup` shows all settings for configuring your email client for connecting to Mailu.
+The menu item `Client setup` shows all settings for configuring your email client for connecting to Mailu.
Website
diff --git a/optional/clamav/Dockerfile b/optional/clamav/Dockerfile
index 1132845f..20cebcdc 100644
--- a/optional/clamav/Dockerfile
+++ b/optional/clamav/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
diff --git a/optional/fetchmail/Dockerfile b/optional/fetchmail/Dockerfile
index a707a54a..506e409a 100644
--- a/optional/fetchmail/Dockerfile
+++ b/optional/fetchmail/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
diff --git a/optional/postgresql/Dockerfile b/optional/postgresql/Dockerfile
index 95048147..0f5034da 100644
--- a/optional/postgresql/Dockerfile
+++ b/optional/postgresql/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
diff --git a/optional/postgresql/start.py b/optional/postgresql/start.py
index 1f2f2a2b..e34e157e 100755
--- a/optional/postgresql/start.py
+++ b/optional/postgresql/start.py
@@ -2,7 +2,6 @@
import anosql
import psycopg2
-import jinja2
import glob
import os
import subprocess
@@ -38,7 +37,6 @@ if not os.listdir("/data"):
rec.write("restore_command = 'gunzip < /backup/wal_archive/%f > %p'\n")
rec.write("standby_mode = off\n")
os.system("chown postgres:postgres /data/recovery.conf")
- #os.system("sudo -u postgres pg_ctl start -D /data -o '-h \"''\" '")
else:
# Bootstrap the database
os.system("sudo -u postgres initdb -D /data")
diff --git a/optional/radicale/Dockerfile b/optional/radicale/Dockerfile
index 400b1a3f..13761164 100644
--- a/optional/radicale/Dockerfile
+++ b/optional/radicale/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
diff --git a/optional/unbound/Dockerfile b/optional/unbound/Dockerfile
index abb45420..2b472d44 100644
--- a/optional/unbound/Dockerfile
+++ b/optional/unbound/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.12
+ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
diff --git a/optional/unbound/unbound.conf b/optional/unbound/unbound.conf
index 8abd4325..6c8fc64d 100644
--- a/optional/unbound/unbound.conf
+++ b/optional/unbound/unbound.conf
@@ -2,7 +2,7 @@ server:
verbosity: 1
interface: 0.0.0.0
interface: ::0
- logfile: /dev/stdout
+ logfile: ""
do-ip4: yes
do-ip6: yes
do-udp: yes
diff --git a/setup/Dockerfile b/setup/Dockerfile
index 2b3c3c6c..5775ab6b 100644
--- a/setup/Dockerfile
+++ b/setup/Dockerfile
@@ -1,4 +1,4 @@
-ARG DISTRO=alpine:3.10
+ARG DISTRO=alpine:3.14
FROM $DISTRO
RUN mkdir -p /app
diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml
index 155e1180..08bba13b 100644
--- a/setup/flavors/compose/docker-compose.yml
+++ b/setup/flavors/compose/docker-compose.yml
@@ -85,6 +85,7 @@ services:
antispam:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-{{ version }}}
+ hostname: antispam
restart: always
env_file: {{ env }}
volumes:
diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env
index 44452e36..d45f5517 100644
--- a/setup/flavors/compose/mailu.env
+++ b/setup/flavors/compose/mailu.env
@@ -86,7 +86,7 @@ WELCOME_SUBJECT={{ welcome_subject or 'Welcome to your new email account' }}
WELCOME_BODY={{ welcome_body or 'Welcome to your new email account, if you can read this, then it is configured properly!' }}
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION={{ compression }}
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL={{ compression_level }}
@@ -175,3 +175,10 @@ DB_HOST={{ db_url }}
DB_NAME={{ db_name }}
{% endif %}
+{% if (postgresql == 'external' or db_flavor == 'mysql') and webmail_type == 'roundcube' %}
+ROUNDCUBE_DB_FLAVOR={{ db_flavor }}
+ROUNDCUBE_DB_USER={{ roundcube_db_user }}
+ROUNDCUBE_DB_PW={{ roundcube_db_pw }}
+ROUNDCUBE_DB_HOST={{ roundcube_db_url }}
+ROUNDCUBE_DB_NAME={{ roundcube_db_name }}
+{% endif %}
diff --git a/setup/flavors/stack/docker-compose.yml b/setup/flavors/stack/docker-compose.yml
index d9c5cd4f..df1fe7b4 100644
--- a/setup/flavors/stack/docker-compose.yml
+++ b/setup/flavors/stack/docker-compose.yml
@@ -70,6 +70,7 @@ services:
antispam:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-{{ version }}}
+ hostname: antispam
env_file: {{ env }}
volumes:
- "{{ root }}/filter:/var/lib/rspamd"
diff --git a/setup/requirements.txt b/setup/requirements.txt
index b6bf2120..f2eb2157 100644
--- a/setup/requirements.txt
+++ b/setup/requirements.txt
@@ -1,4 +1,4 @@
-flask
-flask-bootstrap
-redis
-gunicorn
+Flask==1.0.2
+Flask-Bootstrap==3.3.7.1
+gunicorn==19.9.0
+redis==3.2.1
diff --git a/setup/server.py b/setup/server.py
index 0d58fa25..5be1fc83 100644
--- a/setup/server.py
+++ b/setup/server.py
@@ -54,11 +54,11 @@ def build_app(path):
@app.context_processor
def app_context():
return dict(
- versions=os.getenv("VERSIONS","master").split(','),
+ versions=os.getenv("VERSIONS","master").split(','),
stable_version = os.getenv("stable_version", "master")
)
- prefix_bp = flask.Blueprint(version, __name__)
+ prefix_bp = flask.Blueprint(version.replace(".", "_"), __name__)
prefix_bp.jinja_loader = jinja2.ChoiceLoader([
jinja2.FileSystemLoader(os.path.join(path, "templates")),
jinja2.FileSystemLoader(os.path.join(path, "flavors"))
diff --git a/setup/static/render.js b/setup/static/render.js
index a817c4f0..0a0a6675 100644
--- a/setup/static/render.js
+++ b/setup/static/render.js
@@ -57,6 +57,13 @@ $(document).ready(function() {
$("#db_pw").prop('required',true);
$("#db_url").prop('required',true);
$("#db_name").prop('required',true);
+ if ($("#webmail").val() == 'roundcube') {
+ $("#roundcube_external_db").show();
+ $("#roundcube_db_user").prop('required',true);
+ $("#roundcube_db_pw").prop('required',true);
+ $("#roundcube_db_url").prop('required',true);
+ $("#roundcube_db_name").prop('required',true);
+ }
} else if (this.value == 'mysql') {
$("#postgres_db").hide();
$("#external_db").show();
@@ -64,6 +71,13 @@ $(document).ready(function() {
$("#db_pw").prop('required',true);
$("#db_url").prop('required',true);
$("#db_name").prop('required',true);
+ if ($("#webmail").val() == 'roundcube') {
+ $("#roundcube_external_db").show();
+ $("#roundcube_db_user").prop('required',true);
+ $("#roundcube_db_pw").prop('required',true);
+ $("#roundcube_db_url").prop('required',true);
+ $("#roundcube_db_name").prop('required',true);
+ }
}
});
$("#external_psql").change(function() {
@@ -73,6 +87,13 @@ $(document).ready(function() {
$("#db_pw").prop('required',true);
$("#db_url").prop('required',true);
$("#db_name").prop('required',true);
+ if ($("#webmail").val() == 'roundcube') {
+ $("#roundcube_external_db").show();
+ $("#roundcube_db_user").prop('required',true);
+ $("#roundcube_db_pw").prop('required',true);
+ $("#roundcube_db_url").prop('required',true);
+ $("#roundcube_db_name").prop('required',true);
+ }
} else {
$("#external_db").hide();
}
diff --git a/setup/templates/steps/compose/02_services.html b/setup/templates/steps/compose/02_services.html
index 20d4d7cb..5118c304 100644
--- a/setup/templates/steps/compose/02_services.html
+++ b/setup/templates/steps/compose/02_services.html
@@ -59,7 +59,7 @@ the security implications caused by such an increase of attack surface.
Fetchmail allows users to retrieve mail from an external mail-server via IMAP/POP3 and puts it in their inbox.
-
+
diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html
index 29d8dddd..72b83915 100644
--- a/setup/templates/steps/config.html
+++ b/setup/templates/steps/config.html
@@ -50,8 +50,8 @@ Or in plain english: if receivers start to classify your mail as spam, this post
-
/ minute
+
/ minute
@@ -83,7 +83,7 @@ manage your email domains, users, etc.
diff --git a/setup/templates/steps/stack/02_services.html b/setup/templates/steps/stack/02_services.html
index 3f5186b0..6fce0ae6 100644
--- a/setup/templates/steps/stack/02_services.html
+++ b/setup/templates/steps/stack/02_services.html
@@ -55,7 +55,7 @@ the security implications caused by such an increase of attack surface.
Fetchmail allows users to retrieve mail from an external mail-server via IMAP/POP3 and puts it in their inbox.
-
+
diff --git a/tests/build_arm.sh b/tests/build_arm.sh
index 04836ddb..32dba421 100755
--- a/tests/build_arm.sh
+++ b/tests/build_arm.sh
@@ -1,6 +1,6 @@
#!/bin/bash -x
-ALPINE_VER="3.10"
+ALPINE_VER="3.14"
DISTRO="balenalib/rpi-alpine:$ALPINE_VER"
# Used for webmails
QEMU="arm"
diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env
index b13e57c5..a78515b8 100644
--- a/tests/compose/core/mailu.env
+++ b/tests/compose/core/mailu.env
@@ -92,7 +92,7 @@ DMARC_RUF=admin
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/tests/compose/fetchmail/mailu.env b/tests/compose/fetchmail/mailu.env
index 636a09a9..afb57751 100644
--- a/tests/compose/fetchmail/mailu.env
+++ b/tests/compose/fetchmail/mailu.env
@@ -92,7 +92,7 @@ DMARC_RUF=admin
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/tests/compose/filters/00_create_users.sh b/tests/compose/filters/00_create_users.sh
new file mode 100755
index 00000000..3c581685
--- /dev/null
+++ b/tests/compose/filters/00_create_users.sh
@@ -0,0 +1,5 @@
+echo "Creating user required for next test ..."
+# Should not fail and update the password; update mode
+docker-compose -f tests/compose/filters/docker-compose.yml exec -T admin flask mailu admin admin mailu.io 'password' --mode=update || exit 1
+docker-compose -f tests/compose/filters/docker-compose.yml exec -T admin flask mailu user user mailu.io 'password' || exit 1
+echo "User created successfully"
diff --git a/tests/compose/filters/mailu.env b/tests/compose/filters/mailu.env
index b6d5ca8f..4c8c219d 100644
--- a/tests/compose/filters/mailu.env
+++ b/tests/compose/filters/mailu.env
@@ -92,7 +92,7 @@ DMARC_RUF=admin
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/tests/compose/rainloop/mailu.env b/tests/compose/rainloop/mailu.env
index 9c31c8bb..1f5fce0c 100644
--- a/tests/compose/rainloop/mailu.env
+++ b/tests/compose/rainloop/mailu.env
@@ -51,7 +51,7 @@ DISABLE_STATISTICS=False
###################################
# Expose the admin interface (value: true, false)
-ADMIN=true
+ADMIN=false
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=rainloop
@@ -92,7 +92,7 @@ DMARC_RUF=admin
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/tests/compose/roundcube/mailu.env b/tests/compose/roundcube/mailu.env
index dc503268..ba153ac2 100644
--- a/tests/compose/roundcube/mailu.env
+++ b/tests/compose/roundcube/mailu.env
@@ -51,7 +51,7 @@ DISABLE_STATISTICS=False
###################################
# Expose the admin interface (value: true, false)
-ADMIN=true
+ADMIN=false
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=roundcube
@@ -92,7 +92,7 @@ DMARC_RUF=admin
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/tests/compose/webdav/mailu.env b/tests/compose/webdav/mailu.env
index 90fb25b1..939f453b 100644
--- a/tests/compose/webdav/mailu.env
+++ b/tests/compose/webdav/mailu.env
@@ -92,7 +92,7 @@ DMARC_RUF=admin
# Maildir Compression
-# choose compression-method, default: none (value: bz2, gz)
+# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
diff --git a/tests/deploy.sh b/tests/deploy.sh
index 21aec444..abb37b6b 100755
--- a/tests/deploy.sh
+++ b/tests/deploy.sh
@@ -3,14 +3,5 @@
# Skip deploy for staging branch
[ "$TRAVIS_BRANCH" = "staging" ] && exit 0
-# Retag in case of `bors try`
-if [ "$TRAVIS_BRANCH" = "testing" ]; then
- export DOCKER_ORG="mailutest"
- # Commit message is like "Try #99".
- # This sets the version tag to "pr-99"
- export MAILU_VERSION="pr-${TRAVIS_COMMIT_MESSAGE//[!0-9]/}"
- docker-compose -f tests/build.yml build
-fi
-
docker login -u $DOCKER_UN -p $DOCKER_PW
docker-compose -f tests/build.yml push
diff --git a/towncrier/newsfragments/1194.feature b/towncrier/newsfragments/1194.feature
new file mode 100644
index 00000000..0cd2a9e9
--- /dev/null
+++ b/towncrier/newsfragments/1194.feature
@@ -0,0 +1 @@
+Add a credential cache to speedup authentication requests.
diff --git a/towncrier/newsfragments/1294.bugfix b/towncrier/newsfragments/1294.bugfix
new file mode 100644
index 00000000..68bb7a8a
--- /dev/null
+++ b/towncrier/newsfragments/1294.bugfix
@@ -0,0 +1 @@
+Ensure that the podop socket is always owned by the postfix user (wasn't the case when build using non-standard base images... typically for arm64)
diff --git a/towncrier/newsfragments/1503.doc b/towncrier/newsfragments/1503.doc
new file mode 100644
index 00000000..9c59feb7
--- /dev/null
+++ b/towncrier/newsfragments/1503.doc
@@ -0,0 +1 @@
+Add documentation for Traefik 2 in Reverse Proxy
\ No newline at end of file
diff --git a/towncrier/newsfragments/1604.feature b/towncrier/newsfragments/1604.feature
new file mode 100644
index 00000000..2b47791a
--- /dev/null
+++ b/towncrier/newsfragments/1604.feature
@@ -0,0 +1 @@
+Add cli commands config-import and config-export
diff --git a/towncrier/newsfragments/1660.bugfix b/towncrier/newsfragments/1660.bugfix
deleted file mode 100644
index a90fb099..00000000
--- a/towncrier/newsfragments/1660.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Don't replace nested headers (typically in attached emails)
diff --git a/towncrier/newsfragments/1686.bugfix b/towncrier/newsfragments/1686.bugfix
deleted file mode 100644
index 932d7d7c..00000000
--- a/towncrier/newsfragments/1686.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix letsencrypt access to certbot for the mail-letsencrypt flavour
diff --git a/towncrier/newsfragments/1694.feature b/towncrier/newsfragments/1694.feature
new file mode 100644
index 00000000..f7e2013e
--- /dev/null
+++ b/towncrier/newsfragments/1694.feature
@@ -0,0 +1 @@
+Support configuring lz4 and zstd compression for dovecot.
diff --git a/towncrier/newsfragments/1720.bugfix b/towncrier/newsfragments/1720.bugfix
deleted file mode 100644
index 0bf2b8e6..00000000
--- a/towncrier/newsfragments/1720.bugfix
+++ /dev/null
@@ -1,2 +0,0 @@
-Fix CVE-2020-25275 and CVE-2020-24386 by using alpine 3.13 for
-dovecot which contains a fixed dovecot version.
diff --git a/towncrier/newsfragments/1760.bugfix b/towncrier/newsfragments/1760.bugfix
new file mode 100644
index 00000000..9d6f38af
--- /dev/null
+++ b/towncrier/newsfragments/1760.bugfix
@@ -0,0 +1,2 @@
+Fix CVE-2021-23240, CVE-2021-3156 and CVE-2021-23239 for postgresql
+by force-upgrading sudo.
diff --git a/towncrier/newsfragments/1783.misc b/towncrier/newsfragments/1783.misc
deleted file mode 100644
index 2ee4c97f..00000000
--- a/towncrier/newsfragments/1783.misc
+++ /dev/null
@@ -1 +0,0 @@
-Switch from client side sessions (cookies) to server-side sessions (Redis). This simplies the security model a lot and allows for an easier recovery should a cookie ever land in the hands of an attacker.
diff --git a/towncrier/newsfragments/1828.misc b/towncrier/newsfragments/1828.misc
new file mode 100644
index 00000000..09da59ad
--- /dev/null
+++ b/towncrier/newsfragments/1828.misc
@@ -0,0 +1 @@
+Switched from Travis to Github actions for CI/CD. Improved CI workflow to perform all tests in parallel.
diff --git a/towncrier/newsfragments/1830.misc b/towncrier/newsfragments/1830.misc
new file mode 100644
index 00000000..6de3aff1
--- /dev/null
+++ b/towncrier/newsfragments/1830.misc
@@ -0,0 +1 @@
+Make CI tests run in parallel.
diff --git a/towncrier/newsfragments/1831.bugfix b/towncrier/newsfragments/1831.bugfix
new file mode 100644
index 00000000..1094be34
--- /dev/null
+++ b/towncrier/newsfragments/1831.bugfix
@@ -0,0 +1 @@
+Fix roundcube environment configuration for databases
\ No newline at end of file
diff --git a/towncrier/newsfragments/1851.feature b/towncrier/newsfragments/1851.feature
new file mode 100644
index 00000000..e01f5cb4
--- /dev/null
+++ b/towncrier/newsfragments/1851.feature
@@ -0,0 +1 @@
+Remove cyrus-sasl-plain as it's not packaged by alpine anymore. SASL-login is still available and used when relaying.
diff --git a/towncrier/newsfragments/1917.bugfix b/towncrier/newsfragments/1917.bugfix
new file mode 100644
index 00000000..68187d61
--- /dev/null
+++ b/towncrier/newsfragments/1917.bugfix
@@ -0,0 +1 @@
+Alpine has removed support for btree and hash in postfix... please use lmdb instead
diff --git a/towncrier/newsfragments/224.feature b/towncrier/newsfragments/224.feature
new file mode 100644
index 00000000..9a2f479b
--- /dev/null
+++ b/towncrier/newsfragments/224.feature
@@ -0,0 +1 @@
+Add instructions on how to create DNS records for email client auto-configuration (RFC6186 style)
diff --git a/towncrier/newsfragments/783.feature b/towncrier/newsfragments/783.feature
new file mode 100644
index 00000000..fcafceef
--- /dev/null
+++ b/towncrier/newsfragments/783.feature
@@ -0,0 +1 @@
+Centralize the authentication of webmails behind the admin interface
diff --git a/webmails/rainloop/Dockerfile b/webmails/rainloop/Dockerfile
index ee040f25..9814413d 100644
--- a/webmails/rainloop/Dockerfile
+++ b/webmails/rainloop/Dockerfile
@@ -3,7 +3,7 @@ ARG QEMU=other
# NOTE: only add file if building for arm
FROM ${ARCH}php:7.4-apache as build_arm
-ONBUILD COPY --from=balenalib/rpi-alpine:3.10 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
+ONBUILD COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
FROM ${ARCH}php:7.4-apache as build_other
@@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y \
# Shared layer between nginx, dovecot, postfix, postgresql, rspamd, unbound, rainloop, roundcube
RUN pip3 install socrate
-ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.14.0/rainloop-community-1.14.0.zip
+ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.16.0/rainloop-community-1.16.0.zip
RUN apt-get update && apt-get install -y \
unzip python3-jinja2 \
@@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists
COPY include.php /var/www/html/include.php
+COPY sso.php /var/www/html/sso.php
COPY php.ini /php.ini
COPY application.ini /application.ini
diff --git a/webmails/rainloop/application.ini b/webmails/rainloop/application.ini
index 5bd9943d..0504f174 100644
--- a/webmails/rainloop/application.ini
+++ b/webmails/rainloop/application.ini
@@ -8,6 +8,10 @@ allow_admin_panel = Off
[labs]
allow_gravatar = Off
+{% if ADMIN == "true" %}
+custom_login_link='sso.php'
+custom_logout_link='{{ WEB_ADMIN }}/ui/logout'
+{% endif %}
[contacts]
enable = On
diff --git a/webmails/rainloop/sso.php b/webmails/rainloop/sso.php
new file mode 100644
index 00000000..2415f45c
--- /dev/null
+++ b/webmails/rainloop/sso.php
@@ -0,0 +1,31 @@
+ /usr/local/etc/php/conf.d/timezone.ini \
&& rm -rf /var/www/html/ \
&& cd /var/www \
- && curl -L -O ${ROUNDCUBE_URL} \
- && curl -L -O ${CARDDAV_URL} \
- && tar -xf *.tar.gz \
- && tar -xf *.tar.bz2 \
- && rm -f *.tar.gz \
- && rm -f *.tar.bz2 \
+ && curl -sL ${ROUNDCUBE_URL} | tar xz \
+ && curl -sL ${CARDDAV_URL} | tar xz \
&& mv roundcubemail-* html \
&& mv carddav html/plugins/ \
&& cd html \
@@ -46,6 +42,7 @@ RUN apt-get update && apt-get install -y \
COPY php.ini /php.ini
COPY config.inc.php /var/www/html/config/
+COPY mailu.php /var/www/html/plugins/mailu/mailu.php
COPY start.py /start.py
EXPOSE 80/tcp
diff --git a/webmails/roundcube/config.inc.php b/webmails/roundcube/config.inc.php
index 627b96a7..797f229c 100644
--- a/webmails/roundcube/config.inc.php
+++ b/webmails/roundcube/config.inc.php
@@ -36,7 +36,11 @@ $config['managesieve_host'] = $imap;
$config['managesieve_usetls'] = false;
// Customization settings
-$config['support_url'] = getenv('WEB_ADMIN') ? '../..' . getenv('WEB_ADMIN') : '';
+if (filter_var(getenv('ADMIN'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) {
+ array_push($config['plugins'], 'mailu');
+ $config['support_url'] = getenv('WEB_ADMIN') ? '../..' . getenv('WEB_ADMIN') : '';
+ $config['sso_logout_url'] = getenv('WEB_ADMIN').'/ui/logout';
+}
$config['product_name'] = 'Mailu Webmail';
// We access the IMAP and SMTP servers locally with internal names, SSL
diff --git a/webmails/roundcube/mailu.php b/webmails/roundcube/mailu.php
new file mode 100644
index 00000000..bb4d65e9
--- /dev/null
+++ b/webmails/roundcube/mailu.php
@@ -0,0 +1,59 @@
+add_hook('startup', array($this, 'startup'));
+ $this->add_hook('authenticate', array($this, 'authenticate'));
+ $this->add_hook('login_after', array($this, 'login'));
+ $this->add_hook('login_failed', array($this, 'login_failed'));
+ $this->add_hook('logout_after', array($this, 'logout'));
+ }
+
+ function startup($args)
+ {
+ if (empty($_SESSION['user_id'])) {
+ $args['action'] = 'login';
+ }
+
+ return $args;
+ }
+
+ function authenticate($args)
+ {
+ if (!in_array('HTTP_X_REMOTE_USER', $_SERVER) || !in_array('HTTP_X_REMOTE_USER_TOKEN', $_SERVER)) {
+ header('HTTP/1.0 403 Forbidden');
+ die();
+ }
+ $args['user'] = $_SERVER['HTTP_X_REMOTE_USER'];
+ $args['pass'] = $_SERVER['HTTP_X_REMOTE_USER_TOKEN'];
+
+ $args['cookiecheck'] = false;
+ $args['valid'] = true;
+
+ return $args;
+ }
+
+ function logout($args) {
+ // Redirect to global SSO logout path.
+ $this->load_config();
+
+ $sso_logout_url = rcmail::get_instance()->config->get('sso_logout_url');
+ header("Location: " . $sso_logout_url, true);
+ exit;
+ }
+
+ function login($args)
+ {
+ header('Location: index.php');
+ exit();
+ }
+ function login_failed($args)
+ {
+ header('Location: sso.php');
+ exit();
+ }
+
+}
diff --git a/webmails/roundcube/start.py b/webmails/roundcube/start.py
index 649f3324..cd42ba06 100755
--- a/webmails/roundcube/start.py
+++ b/webmails/roundcube/start.py
@@ -8,41 +8,42 @@ import subprocess
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
-os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))
+os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT")) * 0.66 / 1048576))
-db_flavor=os.environ.get("ROUNDCUBE_DB_FLAVOR",os.environ.get("DB_FLAVOR","sqlite"))
-if db_flavor=="sqlite":
- os.environ["DB_DSNW"]="sqlite:////data/roundcube.db"
-elif db_flavor=="mysql":
- os.environ["DB_DSNW"]="mysql://%s:%s@%s/%s" % (
- os.environ.get("ROUNDCUBE_DB_USER","roundcube"),
+db_flavor = os.environ.get("ROUNDCUBE_DB_FLAVOR", "sqlite")
+if db_flavor == "sqlite":
+ os.environ["DB_DSNW"] = "sqlite:////data/roundcube.db"
+elif db_flavor == "mysql":
+ os.environ["DB_DSNW"] = "mysql://%s:%s@%s/%s" % (
+ os.environ.get("ROUNDCUBE_DB_USER", "roundcube"),
os.environ.get("ROUNDCUBE_DB_PW"),
- os.environ.get("ROUNDCUBE_DB_HOST",os.environ.get("DB_HOST","database")),
- os.environ.get("ROUNDCUBE_DB_NAME","roundcube")
- )
-elif db_flavor=="postgresql":
- os.environ["DB_DSNW"]="pgsql://%s:%s@%s/%s" % (
- os.environ.get("ROUNDCUBE_DB_USER","roundcube"),
+ os.environ.get("ROUNDCUBE_DB_HOST", "database"),
+ os.environ.get("ROUNDCUBE_DB_NAME", "roundcube")
+ )
+elif db_flavor == "postgresql":
+ os.environ["DB_DSNW"] = "pgsql://%s:%s@%s/%s" % (
+ os.environ.get("ROUNDCUBE_DB_USER", "roundcube"),
os.environ.get("ROUNDCUBE_DB_PW"),
- os.environ.get("ROUNDCUBE_DB_HOST",os.environ.get("DB_HOST","database")),
- os.environ.get("ROUNDCUBE_DB_NAME","roundcube")
- )
+ os.environ.get("ROUNDCUBE_DB_HOST", "database"),
+ os.environ.get("ROUNDCUBE_DB_NAME", "roundcube")
+ )
else:
- print("Unknown ROUNDCUBE_DB_FLAVOR: %s",db_flavor)
+ print("Unknown ROUNDCUBE_DB_FLAVOR: %s", db_flavor)
exit(1)
-
-
conf.jinja("/php.ini", os.environ, "/usr/local/etc/php/conf.d/roundcube.ini")
# Create dirs, setup permissions
os.system("mkdir -p /data/gpg /var/www/html/logs")
os.system("touch /var/www/html/logs/errors.log")
os.system("chown -R www-data:www-data /var/www/html/logs")
+os.system("chmod -R a+rX /var/www/html/")
+os.system("ln -sf /var/www/html/index.php /var/www/html/sso.php")
try:
print("Initializing database")
- result=subprocess.check_output(["/var/www/html/bin/initdb.sh","--dir","/var/www/html/SQL"],stderr=subprocess.STDOUT)
+ result = subprocess.check_output(["/var/www/html/bin/initdb.sh", "--dir", "/var/www/html/SQL"],
+ stderr=subprocess.STDOUT)
print(result.decode())
except subprocess.CalledProcessError as e:
if "already exists" in e.stdout.decode():
@@ -53,7 +54,7 @@ except subprocess.CalledProcessError as e:
try:
print("Upgrading database")
- subprocess.check_call(["/var/www/html/bin/update.sh","--version=?","-y"],stderr=subprocess.STDOUT)
+ subprocess.check_call(["/var/www/html/bin/update.sh", "--version=?", "-y"], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
quit(1)
@@ -61,7 +62,7 @@ except subprocess.CalledProcessError as e:
os.system("chown -R www-data:www-data /data")
# Tail roundcube logs
-subprocess.Popen(["tail","-f","-n","0","/var/www/html/logs/errors.log"])
+subprocess.Popen(["tail", "-f", "-n", "0", "/var/www/html/logs/errors.log"])
# Run apache
os.execv("/usr/local/bin/apache2-foreground", ["apache2-foreground"])